expo-harmony-toolkit 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.en.md +20 -9
  2. package/README.md +22 -11
  3. package/build/core/autolinking.d.ts +10 -0
  4. package/build/core/autolinking.js +421 -0
  5. package/build/core/build/commands.d.ts +19 -0
  6. package/build/core/build/commands.js +61 -0
  7. package/build/core/build/compatibilityShims.d.ts +1 -0
  8. package/build/core/build/compatibilityShims.js +272 -0
  9. package/build/core/build/localHar.d.ts +13 -0
  10. package/build/core/build/localHar.js +337 -0
  11. package/build/core/build/reporting.d.ts +19 -0
  12. package/build/core/build/reporting.js +67 -0
  13. package/build/core/build/rnohCompatibility.d.ts +1 -0
  14. package/build/core/build/rnohCompatibility.js +38 -0
  15. package/build/core/build.d.ts +4 -12
  16. package/build/core/build.js +70 -762
  17. package/build/core/constants.d.ts +2 -1
  18. package/build/core/constants.js +3 -2
  19. package/build/core/dependencyInspection.d.ts +2 -0
  20. package/build/core/dependencyInspection.js +112 -0
  21. package/build/core/env.js +1 -1
  22. package/build/core/report.js +33 -10
  23. package/build/core/signing.d.ts +2 -0
  24. package/build/core/signing.js +30 -0
  25. package/build/core/template/capabilityRegistry.d.ts +9 -0
  26. package/build/core/template/capabilityRegistry.js +84 -0
  27. package/build/core/template/expoModulesCoreShim.d.ts +2 -0
  28. package/build/core/template/expoModulesCoreShim.js +279 -0
  29. package/build/core/template/metro.d.ts +2 -0
  30. package/build/core/template/metro.js +213 -0
  31. package/build/core/template/nativeFiles.d.ts +8 -0
  32. package/build/core/template/nativeFiles.js +333 -0
  33. package/build/core/template/renderers/camera.d.ts +4 -0
  34. package/build/core/template/renderers/camera.js +612 -0
  35. package/build/core/template/renderers/fileSystem.d.ts +4 -0
  36. package/build/core/template/renderers/fileSystem.js +814 -0
  37. package/build/core/template/renderers/imagePicker.d.ts +4 -0
  38. package/build/core/template/renderers/imagePicker.js +753 -0
  39. package/build/core/template/renderers/location.d.ts +4 -0
  40. package/build/core/template/renderers/location.js +836 -0
  41. package/build/core/template/runtimeShims.d.ts +2 -0
  42. package/build/core/template/runtimeShims.js +192 -0
  43. package/build/core/template/support.d.ts +11 -0
  44. package/build/core/template/support.js +108 -0
  45. package/build/core/template.d.ts +2 -6
  46. package/build/core/template.js +47 -3966
  47. package/build/data/capabilities.js +45 -19
  48. package/build/data/dependencyCatalog.js +10 -0
  49. package/build/data/publicDocs.d.ts +11 -0
  50. package/build/data/publicDocs.js +28 -0
  51. package/build/data/validatedMatrices.js +9 -1
  52. package/build/docs/render.d.ts +9 -0
  53. package/build/docs/render.js +248 -0
  54. package/build/types.d.ts +12 -0
  55. package/docs/cli-build.md +1 -1
  56. package/docs/npm-release.md +1 -0
  57. package/docs/official-app-shell-sample.md +2 -1
  58. package/docs/official-minimal-sample.md +2 -1
  59. package/docs/official-native-capabilities-sample.md +28 -47
  60. package/docs/official-ui-stack-sample.md +2 -1
  61. package/docs/signing-and-release.md +3 -0
  62. package/docs/support-matrix.md +38 -16
  63. package/package.json +13 -5
  64. package/docs/v1.5.1-acceptance.md +0 -385
  65. package/docs/v1.6.0-acceptance.md +0 -193
  66. package/docs/v1.7.0-acceptance.md +0 -111
  67. package/docs/v1.7.1-acceptance.md +0 -111
@@ -0,0 +1,836 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderExpoHarmonyLocationTurboModule = renderExpoHarmonyLocationTurboModule;
4
+ exports.renderExpoLocationHarmonyAdapterShim = renderExpoLocationHarmonyAdapterShim;
5
+ exports.renderExpoLocationPreviewShim = renderExpoLocationPreviewShim;
6
+ function renderExpoHarmonyLocationTurboModule() {
7
+ return `import type { Permissions } from '@ohos.abilityAccessCtrl';
8
+ import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
9
+ import geoLocationManager from '@ohos.geoLocationManager';
10
+ import { sensor } from '@kit.SensorServiceKit';
11
+ import { AnyThreadTurboModuleContext, AnyThreadTurboModule } from '@rnoh/react-native-openharmony/ts';
12
+
13
+ type PermissionResponse = {
14
+ status: 'granted' | 'denied' | 'undetermined';
15
+ granted: boolean;
16
+ canAskAgain: boolean;
17
+ expires: 'never';
18
+ android: {
19
+ accuracy: 'fine' | 'coarse' | 'none';
20
+ };
21
+ };
22
+
23
+ type ExpoLocationObject = {
24
+ coords: {
25
+ latitude: number;
26
+ longitude: number;
27
+ altitude: number | null;
28
+ accuracy: number | null;
29
+ altitudeAccuracy: number | null;
30
+ heading: number | null;
31
+ speed: number | null;
32
+ };
33
+ timestamp: number;
34
+ mocked: false;
35
+ };
36
+
37
+ type ProviderStatus = {
38
+ locationServicesEnabled: boolean;
39
+ backgroundModeEnabled: boolean;
40
+ gpsAvailable: boolean;
41
+ networkAvailable: boolean;
42
+ passiveAvailable: boolean;
43
+ };
44
+
45
+ type ReverseGeocodeResult = {
46
+ city: string | null;
47
+ district: string | null;
48
+ streetNumber: string | null;
49
+ street: string | null;
50
+ region: string | null;
51
+ subregion: string | null;
52
+ country: string | null;
53
+ postalCode: string | null;
54
+ name: string | null;
55
+ isoCountryCode: string | null;
56
+ timezone: null;
57
+ formattedAddress: string | null;
58
+ };
59
+
60
+ type GeocodeResult = {
61
+ latitude: number;
62
+ longitude: number;
63
+ altitude?: number;
64
+ accuracy?: number;
65
+ };
66
+
67
+ type HeadingObject = {
68
+ magHeading: number;
69
+ trueHeading: number | null;
70
+ accuracy: number;
71
+ };
72
+
73
+ export class ExpoHarmonyLocationTurboModule extends AnyThreadTurboModule {
74
+ public static readonly NAME = 'ExpoHarmonyLocation';
75
+
76
+ private readonly atManager = abilityAccessCtrl.createAtManager();
77
+ private nextWatchId = 1;
78
+
79
+ getConstants(): Record<string, never> {
80
+ return {};
81
+ }
82
+
83
+ async getForegroundPermissionStatus(): Promise<PermissionResponse> {
84
+ return this.getLocationPermissionResponse();
85
+ }
86
+
87
+ async requestForegroundPermission(): Promise<PermissionResponse> {
88
+ return this.requestLocationPermissionResponse();
89
+ }
90
+
91
+ async getBackgroundPermissionStatus(): Promise<PermissionResponse> {
92
+ const backgroundStatus = this.resolvePermissionStatus('ohos.permission.LOCATION_IN_BACKGROUND');
93
+ return this.buildBackgroundPermissionResponse(backgroundStatus);
94
+ }
95
+
96
+ async requestBackgroundPermission(): Promise<PermissionResponse> {
97
+ await this.atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, [
98
+ 'ohos.permission.LOCATION_IN_BACKGROUND',
99
+ ]);
100
+ return this.getBackgroundPermissionStatus();
101
+ }
102
+
103
+ async hasServicesEnabled(): Promise<boolean> {
104
+ return geoLocationManager.isLocationEnabled();
105
+ }
106
+
107
+ async getProviderStatus(): Promise<ProviderStatus> {
108
+ const locationServicesEnabled = geoLocationManager.isLocationEnabled();
109
+
110
+ return {
111
+ locationServicesEnabled,
112
+ backgroundModeEnabled:
113
+ this.resolvePermissionStatus('ohos.permission.LOCATION_IN_BACKGROUND') ===
114
+ abilityAccessCtrl.PermissionStatus.GRANTED,
115
+ gpsAvailable: locationServicesEnabled,
116
+ networkAvailable: locationServicesEnabled,
117
+ passiveAvailable: locationServicesEnabled,
118
+ };
119
+ }
120
+
121
+ async getCurrentPosition(options?: { accuracy?: number }): Promise<ExpoLocationObject> {
122
+ const location = await geoLocationManager.getCurrentLocation(
123
+ this.createCurrentLocationRequest(options?.accuracy),
124
+ );
125
+ return this.normalizeLocation(location);
126
+ }
127
+
128
+ async getLastKnownPosition(_options?: Record<string, unknown>): Promise<ExpoLocationObject | null> {
129
+ try {
130
+ const location = geoLocationManager.getLastLocation();
131
+ return this.hasCoordinates(location) ? this.normalizeLocation(location) : null;
132
+ } catch (_error) {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ async geocode(address: string): Promise<GeocodeResult[]> {
138
+ const addresses = await geoLocationManager.getAddressesFromLocationName({
139
+ description: address,
140
+ locale: 'en-US',
141
+ maxItems: 5,
142
+ });
143
+
144
+ return addresses.map((addressEntry) => ({
145
+ latitude: Number(addressEntry.latitude ?? 0),
146
+ longitude: Number(addressEntry.longitude ?? 0),
147
+ }));
148
+ }
149
+
150
+ async reverseGeocode(location: { latitude: number; longitude: number }): Promise<ReverseGeocodeResult[]> {
151
+ const addresses = await geoLocationManager.getAddressesFromLocation({
152
+ latitude: Number(location.latitude),
153
+ longitude: Number(location.longitude),
154
+ locale: 'en-US',
155
+ maxItems: 5,
156
+ });
157
+
158
+ return addresses.map((addressEntry) => ({
159
+ city: this.readAddressString(addressEntry, ['locality']),
160
+ district: this.readAddressString(addressEntry, ['subLocality']),
161
+ streetNumber: this.readAddressString(addressEntry, ['subThoroughfare', 'streetNumber']),
162
+ street: this.readAddressString(addressEntry, ['thoroughfare', 'street']),
163
+ region: this.readAddressString(addressEntry, ['administrativeArea', 'region']),
164
+ subregion: this.readAddressString(addressEntry, ['subAdministrativeArea', 'subregion']),
165
+ country: this.readAddressString(addressEntry, ['countryName', 'country']),
166
+ postalCode: this.readAddressString(addressEntry, ['postalCode']),
167
+ name: this.readAddressString(addressEntry, ['placeName', 'name']),
168
+ isoCountryCode: this.readAddressString(addressEntry, ['countryCode', 'isoCountryCode']),
169
+ timezone: null,
170
+ formattedAddress: this.readAddressString(addressEntry, ['addressUrl']) ?? this.joinAddressDescriptions(addressEntry),
171
+ }));
172
+ }
173
+
174
+ async startWatchPosition(
175
+ options?: { accuracy?: number },
176
+ _listenerConfig?: Record<string, unknown>,
177
+ ): Promise<{ watchId: number; initialLocation: ExpoLocationObject | null }> {
178
+ const watchId = this.nextWatchId++;
179
+ return {
180
+ watchId,
181
+ initialLocation: await this.getLastKnownPosition(options ?? {}),
182
+ };
183
+ }
184
+
185
+ async stopWatchPosition(_watchId: number): Promise<void> {
186
+ return;
187
+ }
188
+
189
+ async getHeading(): Promise<HeadingObject> {
190
+ return this.readHeadingSnapshot();
191
+ }
192
+
193
+ async startWatchHeading(
194
+ _listenerConfig?: Record<string, unknown>,
195
+ ): Promise<{ watchId: number; initialHeading: HeadingObject }> {
196
+ const watchId = this.nextWatchId++;
197
+ return {
198
+ watchId,
199
+ initialHeading: await this.getHeading(),
200
+ };
201
+ }
202
+
203
+ async stopWatchHeading(_watchId: number): Promise<void> {
204
+ return;
205
+ }
206
+
207
+ private async getLocationPermissionResponse(): Promise<PermissionResponse> {
208
+ const approximateStatus = this.resolvePermissionStatus('ohos.permission.APPROXIMATELY_LOCATION');
209
+ const preciseStatus = this.resolvePermissionStatus('ohos.permission.LOCATION');
210
+
211
+ return this.buildPermissionResponse(approximateStatus, preciseStatus);
212
+ }
213
+
214
+ private async buildBackgroundPermissionResponse(
215
+ backgroundStatus: abilityAccessCtrl.PermissionStatus,
216
+ ): Promise<PermissionResponse> {
217
+ const foregroundPermission = await this.getLocationPermissionResponse();
218
+ const granted = backgroundStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
219
+ const denied =
220
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
221
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
222
+ backgroundStatus === abilityAccessCtrl.PermissionStatus.INVALID;
223
+
224
+ return {
225
+ ...foregroundPermission,
226
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
227
+ granted,
228
+ canAskAgain: !denied,
229
+ };
230
+ }
231
+
232
+ private async requestLocationPermissionResponse(): Promise<PermissionResponse> {
233
+ await this.atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, [
234
+ 'ohos.permission.APPROXIMATELY_LOCATION',
235
+ 'ohos.permission.LOCATION',
236
+ ]);
237
+
238
+ return this.getLocationPermissionResponse();
239
+ }
240
+
241
+ private resolvePermissionStatus(permissionName: Permissions): abilityAccessCtrl.PermissionStatus {
242
+ const atManagerWithSelfStatus = this.atManager as abilityAccessCtrl.AtManager & {
243
+ getSelfPermissionStatus?: (permission: Permissions) => abilityAccessCtrl.PermissionStatus;
244
+ };
245
+
246
+ if (typeof atManagerWithSelfStatus.getSelfPermissionStatus === 'function') {
247
+ return atManagerWithSelfStatus.getSelfPermissionStatus(permissionName);
248
+ }
249
+
250
+ const accessTokenId = this.ctx.uiAbilityContext.abilityInfo.applicationInfo.accessTokenId;
251
+ const grantStatus = this.atManager.checkAccessTokenSync(accessTokenId, permissionName);
252
+
253
+ return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
254
+ ? abilityAccessCtrl.PermissionStatus.GRANTED
255
+ : abilityAccessCtrl.PermissionStatus.NOT_DETERMINED;
256
+ }
257
+
258
+ private buildPermissionResponse(
259
+ approximateStatus: abilityAccessCtrl.PermissionStatus,
260
+ preciseStatus: abilityAccessCtrl.PermissionStatus,
261
+ ): PermissionResponse {
262
+ const coarseGranted = approximateStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
263
+ const fineGranted = preciseStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
264
+ const granted = coarseGranted || fineGranted;
265
+ const denied =
266
+ (approximateStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
267
+ approximateStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
268
+ approximateStatus === abilityAccessCtrl.PermissionStatus.INVALID) &&
269
+ (preciseStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
270
+ preciseStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
271
+ preciseStatus === abilityAccessCtrl.PermissionStatus.INVALID);
272
+
273
+ return {
274
+ status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
275
+ granted,
276
+ canAskAgain: !denied,
277
+ expires: 'never',
278
+ android: {
279
+ accuracy: fineGranted ? 'fine' : coarseGranted ? 'coarse' : 'none',
280
+ },
281
+ };
282
+ }
283
+
284
+ private createCurrentLocationRequest(accuracy?: number): geoLocationManager.CurrentLocationRequest {
285
+ if (typeof accuracy === 'number' && accuracy >= 4) {
286
+ return {
287
+ priority: geoLocationManager.LocationRequestPriority.ACCURACY,
288
+ };
289
+ }
290
+
291
+ if (typeof accuracy === 'number' && accuracy <= 2) {
292
+ return {
293
+ priority: geoLocationManager.LocationRequestPriority.LOW_POWER,
294
+ };
295
+ }
296
+
297
+ return {
298
+ priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
299
+ };
300
+ }
301
+
302
+ private hasCoordinates(location: geoLocationManager.Location | null | undefined): location is geoLocationManager.Location {
303
+ return (
304
+ !!location &&
305
+ typeof location.latitude === 'number' &&
306
+ typeof location.longitude === 'number'
307
+ );
308
+ }
309
+
310
+ private normalizeLocation(location: geoLocationManager.Location): ExpoLocationObject {
311
+ return {
312
+ coords: {
313
+ latitude: Number(location.latitude),
314
+ longitude: Number(location.longitude),
315
+ altitude: typeof location.altitude === 'number' ? location.altitude : null,
316
+ accuracy: typeof location.accuracy === 'number' ? location.accuracy : null,
317
+ altitudeAccuracy: null,
318
+ heading: typeof location.direction === 'number' ? location.direction : null,
319
+ speed: typeof location.speed === 'number' ? location.speed : null,
320
+ },
321
+ timestamp: Number(location.timeStamp ?? Date.now()),
322
+ mocked: false,
323
+ };
324
+ }
325
+
326
+ private async readHeadingSnapshot(): Promise<HeadingObject> {
327
+ return new Promise((resolve) => {
328
+ try {
329
+ const sensorApi = sensor as unknown as {
330
+ once?: (sensorId: number, callback: (data: Record<string, number>) => void) => void;
331
+ SensorId?: {
332
+ ROTATION_VECTOR?: number;
333
+ };
334
+ };
335
+ const onceFn = sensorApi.once;
336
+ const rotationVectorId = sensorApi.SensorId?.ROTATION_VECTOR;
337
+
338
+ if (typeof onceFn !== 'function' || typeof rotationVectorId !== 'number') {
339
+ resolve({
340
+ magHeading: 0,
341
+ trueHeading: null,
342
+ accuracy: 0,
343
+ });
344
+ return;
345
+ }
346
+
347
+ onceFn(rotationVectorId, (data: Record<string, number>) => {
348
+ const x = Number(data?.x ?? 0);
349
+ const y = Number(data?.y ?? 0);
350
+ const z = Number(data?.z ?? 0);
351
+ const w = Number(data?.w ?? 1);
352
+ const sinyCosp = 2 * (w * z + x * y);
353
+ const cosyCosp = 1 - 2 * (y * y + z * z);
354
+ const magHeading = (Math.atan2(sinyCosp, cosyCosp) * 180 / Math.PI + 360) % 360;
355
+ resolve({
356
+ magHeading,
357
+ trueHeading: magHeading,
358
+ accuracy: 3,
359
+ });
360
+ });
361
+ } catch (_error) {
362
+ resolve({
363
+ magHeading: 0,
364
+ trueHeading: null,
365
+ accuracy: 0,
366
+ });
367
+ }
368
+ });
369
+ }
370
+
371
+ private readAddressString(
372
+ addressEntry: geoLocationManager.GeoAddress,
373
+ candidateKeys: string[],
374
+ ): string | null {
375
+ const addressRecord = addressEntry as Record<string, unknown>;
376
+
377
+ for (const key of candidateKeys) {
378
+ const value = addressRecord[key];
379
+
380
+ if (typeof value === 'string' && value.length > 0) {
381
+ return value;
382
+ }
383
+ }
384
+
385
+ return null;
386
+ }
387
+
388
+ private joinAddressDescriptions(addressEntry: geoLocationManager.GeoAddress): string | null {
389
+ const descriptions = (addressEntry as Record<string, unknown>).descriptions;
390
+
391
+ if (!Array.isArray(descriptions) || descriptions.length === 0) {
392
+ return null;
393
+ }
394
+
395
+ const normalized = descriptions.filter(
396
+ (value): value is string => typeof value === 'string' && value.length > 0,
397
+ );
398
+
399
+ return normalized.length > 0 ? normalized.join(', ') : null;
400
+ }
401
+ }
402
+ `;
403
+ }
404
+ function renderExpoLocationHarmonyAdapterShim(capability) {
405
+ return `'use strict';
406
+
407
+ const { TurboModuleRegistry } = require('react-native');
408
+ const { CodedError } = require('expo-modules-core');
409
+
410
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyLocation';
411
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
412
+
413
+ function createError(code, message) {
414
+ return new CodedError(code, message);
415
+ }
416
+
417
+ function createUnsupportedError(operationName) {
418
+ return createError(
419
+ 'ERR_EXPO_HARMONY_UNSUPPORTED',
420
+ '${capability.packageName} does not implement ' + operationName + ' on HarmonyOS yet.',
421
+ );
422
+ }
423
+
424
+ function requireNativeModule(operationName) {
425
+ if (NATIVE_MODULE) {
426
+ return NATIVE_MODULE;
427
+ }
428
+
429
+ throw createError(
430
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
431
+ '${capability.packageName} expected the ' +
432
+ NATIVE_MODULE_NAME +
433
+ ' TurboModule to be registered, but it was missing while running ' +
434
+ operationName +
435
+ '.',
436
+ );
437
+ }
438
+
439
+ function normalizeNativeError(error) {
440
+ if (error instanceof Error) {
441
+ return error;
442
+ }
443
+
444
+ if (error && typeof error === 'object') {
445
+ const code =
446
+ typeof error.code === 'number' || typeof error.code === 'string'
447
+ ? String(error.code)
448
+ : null;
449
+ const message =
450
+ typeof error.message === 'string' && error.message.length > 0
451
+ ? error.message
452
+ : typeof error.name === 'string' && error.name.length > 0
453
+ ? error.name
454
+ : JSON.stringify(error);
455
+
456
+ return new Error(code ? '[native:' + code + '] ' + message : message);
457
+ }
458
+
459
+ return new Error(String(error));
460
+ }
461
+
462
+ async function invokeNative(methodName, operationName, ...args) {
463
+ try {
464
+ return await requireNativeModule(operationName)[methodName](...args);
465
+ } catch (error) {
466
+ throw normalizeNativeError(error);
467
+ }
468
+ }
469
+
470
+ function normalizePermissionResponse(permissionResponse) {
471
+ return {
472
+ status: permissionResponse?.status ?? 'undetermined',
473
+ granted: permissionResponse?.granted === true,
474
+ canAskAgain: permissionResponse?.canAskAgain !== false,
475
+ expires: permissionResponse?.expires ?? 'never',
476
+ android: permissionResponse?.android ?? { accuracy: 'none' },
477
+ ios: permissionResponse?.ios ?? null,
478
+ };
479
+ }
480
+
481
+ function normalizeLocationObject(location) {
482
+ return {
483
+ coords: {
484
+ latitude: Number(location?.coords?.latitude ?? 0),
485
+ longitude: Number(location?.coords?.longitude ?? 0),
486
+ altitude:
487
+ typeof location?.coords?.altitude === 'number' ? location.coords.altitude : null,
488
+ accuracy:
489
+ typeof location?.coords?.accuracy === 'number' ? location.coords.accuracy : null,
490
+ altitudeAccuracy:
491
+ typeof location?.coords?.altitudeAccuracy === 'number'
492
+ ? location.coords.altitudeAccuracy
493
+ : null,
494
+ heading: typeof location?.coords?.heading === 'number' ? location.coords.heading : null,
495
+ speed: typeof location?.coords?.speed === 'number' ? location.coords.speed : null,
496
+ },
497
+ timestamp: Number(location?.timestamp ?? Date.now()),
498
+ mocked: location?.mocked === true,
499
+ };
500
+ }
501
+
502
+ function normalizeProviderStatus(providerStatus) {
503
+ return {
504
+ locationServicesEnabled: providerStatus?.locationServicesEnabled === true,
505
+ backgroundModeEnabled: providerStatus?.backgroundModeEnabled === true,
506
+ gpsAvailable: providerStatus?.gpsAvailable === true,
507
+ networkAvailable: providerStatus?.networkAvailable === true,
508
+ passiveAvailable: providerStatus?.passiveAvailable === true,
509
+ };
510
+ }
511
+
512
+ function normalizeReverseGeocodeResult(address) {
513
+ return {
514
+ city: address?.city ?? null,
515
+ district: address?.district ?? null,
516
+ streetNumber: address?.streetNumber ?? null,
517
+ street: address?.street ?? null,
518
+ region: address?.region ?? null,
519
+ subregion: address?.subregion ?? null,
520
+ country: address?.country ?? null,
521
+ postalCode: address?.postalCode ?? null,
522
+ name: address?.name ?? null,
523
+ isoCountryCode: address?.isoCountryCode ?? null,
524
+ timezone: address?.timezone ?? null,
525
+ formattedAddress: address?.formattedAddress ?? null,
526
+ };
527
+ }
528
+
529
+ function normalizeGeocodeInput(address) {
530
+ if (typeof address === 'string') {
531
+ return address.trim();
532
+ }
533
+
534
+ if (!address || typeof address !== 'object') {
535
+ return '';
536
+ }
537
+
538
+ const parts = [
539
+ address.name,
540
+ address.streetNumber,
541
+ address.street,
542
+ address.district,
543
+ address.city,
544
+ address.region,
545
+ address.postalCode,
546
+ address.country,
547
+ ]
548
+ .filter((part) => typeof part === 'string' && part.trim().length > 0)
549
+ .map((part) => part.trim());
550
+
551
+ return parts.join(', ');
552
+ }
553
+
554
+ function normalizeGeocodeResults(results) {
555
+ if (!Array.isArray(results)) {
556
+ return [];
557
+ }
558
+
559
+ return results.map((result) => ({
560
+ latitude: Number(result?.latitude ?? 0),
561
+ longitude: Number(result?.longitude ?? 0),
562
+ altitude: typeof result?.altitude === 'number' ? result.altitude : null,
563
+ accuracy: typeof result?.accuracy === 'number' ? result.accuracy : null,
564
+ }));
565
+ }
566
+
567
+ function normalizeHeadingObject(heading) {
568
+ return {
569
+ magHeading: Number(heading?.magHeading ?? 0),
570
+ trueHeading:
571
+ typeof heading?.trueHeading === 'number' && Number.isFinite(heading.trueHeading)
572
+ ? heading.trueHeading
573
+ : null,
574
+ accuracy:
575
+ typeof heading?.accuracy === 'number' && Number.isFinite(heading.accuracy)
576
+ ? heading.accuracy
577
+ : 0,
578
+ };
579
+ }
580
+
581
+ function createSubscription(remove) {
582
+ let active = true;
583
+
584
+ return {
585
+ remove() {
586
+ if (!active) {
587
+ return;
588
+ }
589
+
590
+ active = false;
591
+ remove();
592
+ },
593
+ };
594
+ }
595
+
596
+ module.exports = {
597
+ Accuracy: {
598
+ Lowest: 1,
599
+ Low: 2,
600
+ Balanced: 3,
601
+ High: 4,
602
+ Highest: 5,
603
+ BestForNavigation: 6,
604
+ },
605
+ PermissionStatus: {
606
+ DENIED: 'denied',
607
+ GRANTED: 'granted',
608
+ UNDETERMINED: 'undetermined',
609
+ },
610
+ async getForegroundPermissionsAsync() {
611
+ return normalizePermissionResponse(
612
+ await invokeNative(
613
+ 'getForegroundPermissionStatus',
614
+ 'getForegroundPermissionsAsync',
615
+ ),
616
+ );
617
+ },
618
+ async requestForegroundPermissionsAsync() {
619
+ return normalizePermissionResponse(
620
+ await invokeNative(
621
+ 'requestForegroundPermission',
622
+ 'requestForegroundPermissionsAsync',
623
+ ),
624
+ );
625
+ },
626
+ async getBackgroundPermissionsAsync() {
627
+ return normalizePermissionResponse(
628
+ await invokeNative(
629
+ 'getBackgroundPermissionStatus',
630
+ 'getBackgroundPermissionsAsync',
631
+ ),
632
+ );
633
+ },
634
+ async requestBackgroundPermissionsAsync() {
635
+ return normalizePermissionResponse(
636
+ await invokeNative(
637
+ 'requestBackgroundPermission',
638
+ 'requestBackgroundPermissionsAsync',
639
+ ),
640
+ );
641
+ },
642
+ async hasServicesEnabledAsync() {
643
+ return await invokeNative('hasServicesEnabled', 'hasServicesEnabledAsync');
644
+ },
645
+ async getProviderStatusAsync() {
646
+ return normalizeProviderStatus(
647
+ await invokeNative('getProviderStatus', 'getProviderStatusAsync'),
648
+ );
649
+ },
650
+ async getCurrentPositionAsync(options) {
651
+ return normalizeLocationObject(
652
+ await invokeNative('getCurrentPosition', 'getCurrentPositionAsync', options ?? {}),
653
+ );
654
+ },
655
+ async getLastKnownPositionAsync(options) {
656
+ const location = await invokeNative(
657
+ 'getLastKnownPosition',
658
+ 'getLastKnownPositionAsync',
659
+ options ?? {},
660
+ );
661
+ return location ? normalizeLocationObject(location) : null;
662
+ },
663
+ async geocodeAsync(address) {
664
+ return normalizeGeocodeResults(
665
+ await invokeNative(
666
+ 'geocode',
667
+ 'geocodeAsync',
668
+ normalizeGeocodeInput(address),
669
+ ),
670
+ );
671
+ },
672
+ async reverseGeocodeAsync(location) {
673
+ const results = await invokeNative(
674
+ 'reverseGeocode',
675
+ 'reverseGeocodeAsync',
676
+ {
677
+ latitude: Number(location?.latitude ?? 0),
678
+ longitude: Number(location?.longitude ?? 0),
679
+ },
680
+ );
681
+
682
+ if (!Array.isArray(results)) {
683
+ return [];
684
+ }
685
+
686
+ return results.map(normalizeReverseGeocodeResult);
687
+ },
688
+ async watchPositionAsync(options, callback, errorHandler) {
689
+ if (typeof callback !== 'function') {
690
+ throw createError(
691
+ 'ERR_EXPO_HARMONY_INVALID_LISTENER',
692
+ '${capability.packageName} expected watchPositionAsync to receive a callback.',
693
+ );
694
+ }
695
+
696
+ try {
697
+ const watchResult = await invokeNative(
698
+ 'startWatchPosition',
699
+ 'watchPositionAsync',
700
+ options ?? {},
701
+ null,
702
+ );
703
+ const watchId =
704
+ typeof watchResult?.watchId === 'number' ? watchResult.watchId : Number(Date.now());
705
+
706
+ if (watchResult?.initialLocation) {
707
+ callback(normalizeLocationObject(watchResult.initialLocation));
708
+ }
709
+
710
+ return createSubscription(() => {
711
+ void invokeNative(
712
+ 'stopWatchPosition',
713
+ 'Location.watchPositionAsync.remove',
714
+ watchId,
715
+ ).catch(() => {});
716
+ });
717
+ } catch (error) {
718
+ if (typeof errorHandler === 'function') {
719
+ errorHandler(error);
720
+ }
721
+
722
+ throw error;
723
+ }
724
+ },
725
+ async watchHeadingAsync(callback) {
726
+ if (typeof callback !== 'function') {
727
+ throw createError(
728
+ 'ERR_EXPO_HARMONY_INVALID_LISTENER',
729
+ '${capability.packageName} expected watchHeadingAsync to receive a callback.',
730
+ );
731
+ }
732
+
733
+ const watchResult = await invokeNative(
734
+ 'startWatchHeading',
735
+ 'watchHeadingAsync',
736
+ null,
737
+ );
738
+ const watchId =
739
+ typeof watchResult?.watchId === 'number' ? watchResult.watchId : Number(Date.now());
740
+
741
+ if (watchResult?.initialHeading) {
742
+ callback(normalizeHeadingObject(watchResult.initialHeading));
743
+ }
744
+
745
+ return createSubscription(() => {
746
+ void invokeNative(
747
+ 'stopWatchHeading',
748
+ 'Location.watchHeadingAsync.remove',
749
+ watchId,
750
+ ).catch(() => {});
751
+ });
752
+ },
753
+ async getHeadingAsync() {
754
+ return normalizeHeadingObject(
755
+ await invokeNative('getHeading', 'getHeadingAsync'),
756
+ );
757
+ },
758
+ };
759
+ `;
760
+ }
761
+ function renderExpoLocationPreviewShim(capability) {
762
+ return `'use strict';
763
+
764
+ const { CodedError } = require('expo-modules-core');
765
+
766
+ const PREVIEW_MESSAGE =
767
+ '${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.';
768
+ const DEFAULT_PERMISSION_RESPONSE = {
769
+ status: 'undetermined',
770
+ granted: false,
771
+ canAskAgain: true,
772
+ expires: 'never',
773
+ };
774
+
775
+ function createPreviewError(operationName) {
776
+ return new CodedError(
777
+ 'ERR_EXPO_HARMONY_PREVIEW',
778
+ PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
779
+ );
780
+ }
781
+
782
+ async function unavailable(operationName) {
783
+ throw createPreviewError(operationName);
784
+ }
785
+
786
+ function getPermissionResponse() {
787
+ return { ...DEFAULT_PERMISSION_RESPONSE };
788
+ }
789
+
790
+ module.exports = {
791
+ Accuracy: {
792
+ Lowest: 1,
793
+ Low: 2,
794
+ Balanced: 3,
795
+ High: 4,
796
+ Highest: 5,
797
+ BestForNavigation: 6,
798
+ },
799
+ PermissionStatus: {
800
+ DENIED: 'denied',
801
+ GRANTED: 'granted',
802
+ UNDETERMINED: 'undetermined',
803
+ },
804
+ requestForegroundPermissionsAsync() {
805
+ return unavailable('requestForegroundPermissionsAsync');
806
+ },
807
+ getForegroundPermissionsAsync() {
808
+ return Promise.resolve(getPermissionResponse());
809
+ },
810
+ requestBackgroundPermissionsAsync() {
811
+ return unavailable('requestBackgroundPermissionsAsync');
812
+ },
813
+ getBackgroundPermissionsAsync() {
814
+ return Promise.resolve(getPermissionResponse());
815
+ },
816
+ hasServicesEnabledAsync() {
817
+ return Promise.resolve(false);
818
+ },
819
+ getCurrentPositionAsync(options) {
820
+ return unavailable('getCurrentPositionAsync(' + JSON.stringify(options ?? {}) + ')');
821
+ },
822
+ getLastKnownPositionAsync(options) {
823
+ return unavailable('getLastKnownPositionAsync(' + JSON.stringify(options ?? {}) + ')');
824
+ },
825
+ watchPositionAsync(options) {
826
+ return unavailable('watchPositionAsync(' + JSON.stringify(options ?? {}) + ')');
827
+ },
828
+ geocodeAsync(address) {
829
+ return unavailable('geocodeAsync(' + JSON.stringify(address ?? null) + ')');
830
+ },
831
+ reverseGeocodeAsync(location) {
832
+ return unavailable('reverseGeocodeAsync(' + JSON.stringify(location ?? null) + ')');
833
+ },
834
+ };
835
+ `;
836
+ }