From 70b1907ace2a8c51dd0490885ccbe52479906cf7 Mon Sep 17 00:00:00 2001 From: stelselim Date: Mon, 20 Jul 2026 17:26:15 +0200 Subject: [PATCH 01/10] feat: migrate to new geolocation library and update related functionality --- configs/jsactions/rollup.config.mjs | 3 +- .../nanoflow-actions-native/package.json | 5 +- .../src/geolocation/GetCurrentLocation.ts | 101 ++------------ .../GetCurrentLocationMinimumAccuracy.ts | 132 +++--------------- .../src/geolocation/utils.ts | 49 +++++++ .../typings/Geolocation.d.ts | 22 --- pnpm-lock.yaml | 45 ++++-- 7 files changed, 118 insertions(+), 239 deletions(-) create mode 100644 packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts delete mode 100644 packages/jsActions/nanoflow-actions-native/typings/Geolocation.d.ts diff --git a/configs/jsactions/rollup.config.mjs b/configs/jsactions/rollup.config.mjs index d8691fac1..115f7c3db 100644 --- a/configs/jsactions/rollup.config.mjs +++ b/configs/jsactions/rollup.config.mjs @@ -34,7 +34,8 @@ export default async args => { types: ["mendix-client", "react-native"], allowSyntheticDefaultImports: true, compilerOptions: { - newLine: "CRLF" + newLine: "CRLF", + jsx: "react-native" } }); diff --git a/packages/jsActions/nanoflow-actions-native/package.json b/packages/jsActions/nanoflow-actions-native/package.json index a1b788d35..c74decb91 100644 --- a/packages/jsActions/nanoflow-actions-native/package.json +++ b/packages/jsActions/nanoflow-actions-native/package.json @@ -1,7 +1,7 @@ { "name": "nanoflow-actions-native", "moduleName": "Nanoflow Commons", - "version": "7.1.0", + "version": "7.2.0", "license": "Apache-2.0", "copyright": "© Mendix Technology BV 2022. All rights reserved.", "repository": { @@ -27,9 +27,10 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-community/geolocation": "3.4.0", "invariant": "^2.2.4", "js-base64": "~3.7.2", + "react-native-nitro-geolocation": "1.4.3", + "react-native-nitro-modules": "0.22.0", "react-native-permissions": "5.5.1", "react-native-geocoder": "0.5.0" }, diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index b333c81d4..e2691e192 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -5,15 +5,9 @@ // - the code between BEGIN USER CODE and END USER CODE // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. -import { Big } from "big.js"; -import Geolocation, { - GeolocationError, - GeolocationOptions, - GeolocationResponse -} from "@react-native-community/geolocation"; +import { getCurrentPosition } from "react-native-nitro-geolocation"; -import type { Platform, NativeModules } from "react-native"; -import type { GeoError, GeoPosition, GeoOptions } from "../../typings/Geolocation"; +import { buildLocationOptions, mapPositionToMxObject } from "./utils"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -39,94 +33,23 @@ export async function GetCurrentLocation( ): Promise { // BEGIN USER CODE - let reactNativeModule: { NativeModules: typeof NativeModules; Platform: typeof Platform } | undefined; - let geolocationModule: typeof import("@react-native-community/geolocation").default | Geolocation; - - if (navigator && navigator.product === "ReactNative") { - reactNativeModule = require("react-native"); - - if (!reactNativeModule) { - return Promise.reject(new Error("React Native module could not be found")); - } - - if (reactNativeModule.NativeModules.RNFusedLocation) { - geolocationModule = (await import("@react-native-community/geolocation")).default; - } else if (reactNativeModule.NativeModules.RNCGeolocation) { - geolocationModule = Geolocation; - } else { - return Promise.reject(new Error("Geolocation module could not be found")); - } - } else if (navigator && navigator.geolocation) { - geolocationModule = navigator.geolocation; - } else { - return Promise.reject(new Error("Geolocation module could not be found")); - } - - return new Promise((resolve, reject) => { - const options = getOptions(); - geolocationModule?.getCurrentPosition(onSuccess, onError, options); - - function onSuccess(position: GeolocationResponse | GeoPosition): void { + const options = buildLocationOptions(timeout, maximumAge, highAccuracy); + try { + const position = await getCurrentPosition(options); + return new Promise((resolve, reject) => { mx.data.create({ entity: "NanoflowCommons.Geolocation", callback: mxObject => { - const geolocation = mapPositionToMxObject(mxObject, position); - resolve(geolocation); + resolve(mapPositionToMxObject(mxObject, position)); }, error: () => reject(new Error("Could not create 'NanoflowCommons.Geolocation' object to store location")) }); - } - - function onError(error: GeolocationError | GeoError): void { - return reject(new Error(error.message)); - } - - function getOptions(): GeolocationOptions | GeoOptions { - let timeoutNumber = timeout && Number(timeout.toString()); - const maximumAgeNumber = maximumAge && Number(maximumAge.toString()); - - // If the timeout is 0 or undefined (empty), it causes a crash on iOS. - // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) - // If the timeout is 0; we set timeout to 1 hour (no timeout) - if (reactNativeModule?.Platform.OS === "ios") { - if (timeoutNumber === undefined) { - timeoutNumber = 30000; - } else if (timeoutNumber === 0) { - timeoutNumber = 3600000; - } - } - - return { - timeout: timeoutNumber, - maximumAge: maximumAgeNumber, - enableHighAccuracy: highAccuracy - }; - } - - function mapPositionToMxObject( - mxObject: mendix.lib.MxObject, - position: GeolocationResponse | GeoPosition - ): mendix.lib.MxObject { - mxObject.set("Timestamp", new Date(position.timestamp)); - mxObject.set("Latitude", new Big(position.coords.latitude.toFixed(8))); - mxObject.set("Longitude", new Big(position.coords.longitude.toFixed(8))); - mxObject.set("Accuracy", new Big(position.coords.accuracy.toFixed(8))); - if (position.coords.altitude != null) { - mxObject.set("Altitude", new Big(position.coords.altitude.toFixed(8))); - } - if (position.coords.altitudeAccuracy != null && position.coords.altitudeAccuracy !== -1) { - mxObject.set("AltitudeAccuracy", new Big(position.coords.altitudeAccuracy.toFixed(8))); - } - if (position.coords.heading != null && position.coords.heading !== -1) { - mxObject.set("Heading", new Big(position.coords.heading.toFixed(8))); - } - if (position.coords.speed != null && position.coords.speed !== -1) { - mxObject.set("Speed", new Big(position.coords.speed.toFixed(8))); - } - return mxObject; - } - }); + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return Promise.reject(new Error(`Could not get current location: ${message}`)); + } // END USER CODE } diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts index 7756b3e1b..48fbd130d 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts @@ -5,15 +5,8 @@ // - the code between BEGIN USER CODE and END USER CODE // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. -import { Big } from "big.js"; -import Geolocation, { - GeolocationError, - GeolocationOptions, - GeolocationResponse -} from "@react-native-community/geolocation"; - -import type { Platform, NativeModules } from "react-native"; -import type { GeoError, GeoPosition, GeoOptions } from "../../typings/Geolocation"; +import { watchPosition, unwatch, LocationError, GeolocationResponse } from "react-native-nitro-geolocation"; +import { buildLocationOptions, mapPositionToMxObject } from "./utils"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -25,7 +18,7 @@ import type { GeoError, GeoPosition, GeoOptions } from "../../typings/Geolocatio * * On hybrid and native platforms the permission should be requested with the `RequestLocationPermission` action. * - * For good user experience, disable the nanoflow during action using property `Disabled during action` if you’re using `Call a nanoflow button` to run JS Action `Get current location with minimum accuracy`. + * For good user experience, disable the nanoflow during action using property `Disabled during action` if you're using `Call a nanoflow button` to run JS Action `Get current location with minimum accuracy`. * * Best practices: * https://developers.google.com/web/fundamentals/native-hardware/user-location/ @@ -43,63 +36,16 @@ export async function GetCurrentLocationMinimumAccuracy( ): Promise { // BEGIN USER CODE - let reactNativeModule: { NativeModules: typeof NativeModules; Platform: typeof Platform } | undefined; - let geolocationModule: typeof import("@react-native-community/geolocation").default | Geolocation; - - if (navigator && navigator.product === "ReactNative") { - reactNativeModule = require("react-native"); - - if (!reactNativeModule) { - return Promise.reject(new Error("React Native module could not be found")); - } - - if (reactNativeModule.NativeModules.RNFusedLocation) { - geolocationModule = (await import("@react-native-community/geolocation")).default; - } else if (reactNativeModule.NativeModules.RNCGeolocation) { - geolocationModule = Geolocation; - } else { - return Promise.reject(new Error("Geolocation module could not be found")); - } - } else if (navigator && navigator.geolocation) { - geolocationModule = navigator.geolocation; - } else { - return Promise.reject(new Error("Geolocation module could not be found")); - } - return new Promise((resolve, reject) => { - if (!geolocationModule) { - return reject(new Error("Geolocation module could not be found")); - } - - const options = getOptions(); + const options = buildLocationOptions(timeout, maximumAge, highAccuracy); + let lastAccruedPosition: GeolocationResponse | undefined; - // This action is only required while running in PWA or hybrid. - if (navigator && (!navigator.product || navigator.product !== "ReactNative")) { - // This ensures the browser will not ignore the maximumAge https://stackoverflow.com/questions/3397585/navigator-geolocation-getcurrentposition-sometimes-works-sometimes-doesnt/31916631#31916631 - geolocationModule.getCurrentPosition( - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - () => {}, - {} - ); - } - - const timeoutId = setTimeout(onTimeout, Number(timeout)); - const watchId: number = geolocationModule.watchPosition(onSuccess, onError, options); - let lastAccruedPosition: GeolocationResponse | GeoPosition; - - function createGeolocationObject(position: GeolocationResponse | GeoPosition): void { - mx.data.create({ - entity: "NanoflowCommons.Geolocation", - callback: mxObject => resolve(mapPositionToMxObject(mxObject, position)), - error: () => - reject(new Error("Could not create 'NanoflowCommons.Geolocation' object to store location")) - }); - } + const timeoutMs = timeout ? timeout.toNumber() : 30000; + const timeoutId = setTimeout(onTimeout, timeoutMs); + const token = watchPosition(onSuccess, onError, options); function onTimeout(): void { - geolocationModule?.clearWatch(watchId); + unwatch(token); if (lastAccruedPosition) { createGeolocationObject(lastAccruedPosition); @@ -108,10 +54,10 @@ export async function GetCurrentLocationMinimumAccuracy( } } - function onSuccess(position: GeolocationResponse | GeoPosition): void { + function onSuccess(position: GeolocationResponse): void { if (!minimumAccuracy || Number(minimumAccuracy) >= position.coords.accuracy) { clearTimeout(timeoutId); - geolocationModule?.clearWatch(watchId); + unwatch(token); createGeolocationObject(position); } else { if (!lastAccruedPosition || position.coords.accuracy < lastAccruedPosition.coords.accuracy) { @@ -120,53 +66,19 @@ export async function GetCurrentLocationMinimumAccuracy( } } - function onError(error: GeolocationError | GeoError): void { - return reject(new Error(error.message)); + function onError(error: LocationError): void { + clearTimeout(timeoutId); + unwatch(token); + reject(new Error(error.message)); } - function getOptions(): GeolocationOptions | GeoOptions { - let timeoutNumber = timeout && Number(timeout.toString()); - const maximumAgeNumber = maximumAge && Number(maximumAge.toString()); - - // If the timeout is 0 or undefined (empty), it causes a crash on iOS. - // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) - // If the timeout is 0; we set timeout to 1 hour (no timeout) - if (reactNativeModule?.Platform.OS === "ios") { - if (timeoutNumber === undefined) { - timeoutNumber = 30000; - } else if (timeoutNumber === 0) { - timeoutNumber = 3600000; - } - } - - return { - timeout: timeoutNumber, - maximumAge: maximumAgeNumber, - enableHighAccuracy: highAccuracy - }; - } - - function mapPositionToMxObject( - mxObject: mendix.lib.MxObject, - position: GeolocationResponse | GeoPosition - ): mendix.lib.MxObject { - mxObject.set("Timestamp", new Date(position.timestamp)); - mxObject.set("Latitude", new Big(position.coords.latitude.toFixed(8))); - mxObject.set("Longitude", new Big(position.coords.longitude.toFixed(8))); - mxObject.set("Accuracy", new Big(position.coords.accuracy.toFixed(8))); - if (position.coords.altitude != null) { - mxObject.set("Altitude", new Big(position.coords.altitude.toFixed(8))); - } - if (position.coords.altitudeAccuracy != null && position.coords.altitudeAccuracy !== -1) { - mxObject.set("AltitudeAccuracy", new Big(position.coords.altitudeAccuracy.toFixed(8))); - } - if (position.coords.heading != null && position.coords.heading !== -1) { - mxObject.set("Heading", new Big(position.coords.heading.toFixed(8))); - } - if (position.coords.speed != null && position.coords.speed !== -1) { - mxObject.set("Speed", new Big(position.coords.speed.toFixed(8))); - } - return mxObject; + function createGeolocationObject(position: GeolocationResponse): void { + mx.data.create({ + entity: "NanoflowCommons.Geolocation", + callback: mxObject => resolve(mapPositionToMxObject(mxObject, position)), + error: () => + reject(new Error("Could not create 'NanoflowCommons.Geolocation' object to store location")) + }); } }); diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts new file mode 100644 index 000000000..de6d142b8 --- /dev/null +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts @@ -0,0 +1,49 @@ +import { Big } from "big.js"; +import { Platform } from "react-native"; +import { GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; + +export function buildLocationOptions( + timeout: Big | undefined, + maximumAge: Big | undefined, + highAccuracy: boolean | undefined +): LocationRequestOptions { + let timeoutNumber = timeout ? timeout.toNumber() : undefined; + const maximumAgeNumber = maximumAge ? maximumAge.toNumber() : undefined; + + // If the timeout is 0 or undefined (empty), it causes a crash on iOS. + // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) + // If the timeout is 0; we set timeout to 1 hour (no timeout) + if (Platform.OS === "ios") { + if (timeoutNumber === undefined) { + timeoutNumber = 30000; + } else if (timeoutNumber === 0) { + timeoutNumber = 3600000; + } + } + + return { + timeout: timeoutNumber, + maximumAge: maximumAgeNumber, + accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } + }; +} + +export function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { + mxObject.set("Timestamp", new Date(pos.timestamp)); + mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); + mxObject.set("Longitude", new Big(pos.coords.longitude.toFixed(8))); + mxObject.set("Accuracy", new Big(pos.coords.accuracy.toFixed(8))); + if (pos.coords.altitude != null) { + mxObject.set("Altitude", new Big(pos.coords.altitude.toFixed(8))); + } + if (pos.coords.altitudeAccuracy != null && pos.coords.altitudeAccuracy !== -1) { + mxObject.set("AltitudeAccuracy", new Big(pos.coords.altitudeAccuracy.toFixed(8))); + } + if (pos.coords.heading != null && pos.coords.heading !== -1) { + mxObject.set("Heading", new Big(pos.coords.heading.toFixed(8))); + } + if (pos.coords.speed != null && pos.coords.speed !== -1) { + mxObject.set("Speed", new Big(pos.coords.speed.toFixed(8))); + } + return mxObject; +} diff --git a/packages/jsActions/nanoflow-actions-native/typings/Geolocation.d.ts b/packages/jsActions/nanoflow-actions-native/typings/Geolocation.d.ts deleted file mode 100644 index c92f243d4..000000000 --- a/packages/jsActions/nanoflow-actions-native/typings/Geolocation.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { - AuthorizationLevel, - AuthorizationResult, - GeoError, - GeoPosition, - GeoOptions, - getCurrentPosition, - requestAuthorization, - watchPosition, - clearWatch, - stopObserving -} from "@react-native-community/geolocation"; - -type GeolocationServiceStatic = { - getCurrentPosition: typeof getCurrentPosition; - requestAuthorization: typeof requestAuthorization; - watchPosition: typeof watchPosition; - clearWatch: typeof clearWatch; - stopObserving: typeof stopObserving; -}; - -export type { GeolocationServiceStatic, AuthorizationLevel, AuthorizationResult, GeoError, GeoPosition, GeoOptions }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8d5c6189..74f723a2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,9 +194,6 @@ importers: '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3)) - '@react-native-community/geolocation': - specifier: 3.4.0 - version: 3.4.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) invariant: specifier: ^2.2.4 version: 2.2.4 @@ -206,6 +203,12 @@ importers: react-native-geocoder: specifier: 0.5.0 version: 0.5.0 + react-native-nitro-geolocation: + specifier: 1.4.3 + version: 1.4.3(react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-nitro-modules: + specifier: 0.22.0 + version: 0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) react-native-permissions: specifier: 5.5.1 version: 5.5.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) @@ -2059,13 +2062,6 @@ packages: peerDependencies: react-native: 0.84.1 - '@react-native-community/geolocation@3.4.0': - resolution: {integrity: sha512-bzZH89/cwmpkPMKKveoC72C4JH0yF4St5Ceg/ZM9pA1SqX9MlRIrIrrOGZ/+yi++xAvFDiYfihtn9TvXWU9/rA==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: 19.2.3 - react-native: 0.84.1 - '@react-native-community/netinfo@11.5.2': resolution: {integrity: sha512-/g0m65BtX9HU+bPiCH2517bOHpEIUsGrWFXDzi1a5nNKn5KujQgm04WhL7/OSXWKHyrT8VVtUoJA0XKRxueBpQ==} peerDependencies: @@ -6088,6 +6084,19 @@ packages: react: 19.2.3 react-native: 0.84.1 + react-native-nitro-geolocation@1.4.3: + resolution: {integrity: sha512-8r09M6fjf7krCXRuF3RskuJpcxE8dvwQtRJnlrk+U3tO4v+ltyXhRRGmAMlO2Y/PEv1II8sgnIgil58RaLPI4A==} + peerDependencies: + react: 19.2.3 + react-native: 0.84.1 + react-native-nitro-modules: '*' + + react-native-nitro-modules@0.22.0: + resolution: {integrity: sha512-3obSYG8dgg3D8lEU/xMKL/0r1A3FrKB2tIKa+LWfBaXnMfWwg5n1o2GRcLpbLtmgFPx903JjVuxhFDRAtslW0Q==} + peerDependencies: + react: 19.2.3 + react-native: 0.84.1 + react-native-permissions@5.5.1: resolution: {integrity: sha512-nTKFoj47b6EXNqbbg+8VFwBWMpxF1/UTbrNBLpXkWpt005pH4BeFv/NwpcC1iNhToKBrxQD+5kI0z6+kTYoYWA==} peerDependencies: @@ -8924,11 +8933,6 @@ snapshots: dependencies: react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) - '@react-native-community/geolocation@3.4.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': - dependencies: - react: 19.2.3 - react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) - '@react-native-community/netinfo@11.5.2(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 @@ -13871,6 +13875,17 @@ snapshots: react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) react-native-animatable: 1.3.3 + react-native-nitro-geolocation@1.4.3(react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) + react-native-nitro-modules: 0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + + react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) + react-native-permissions@5.5.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 From c34370baca1655851a10c53a7b9a2ead6b796099 Mon Sep 17 00:00:00 2001 From: stelselim Date: Tue, 21 Jul 2026 10:19:36 +0200 Subject: [PATCH 02/10] fix: update react-native-nitro-modules to version 0.36.1 --- .../nanoflow-actions-native/package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/jsActions/nanoflow-actions-native/package.json b/packages/jsActions/nanoflow-actions-native/package.json index c74decb91..bfcd77854 100644 --- a/packages/jsActions/nanoflow-actions-native/package.json +++ b/packages/jsActions/nanoflow-actions-native/package.json @@ -30,7 +30,7 @@ "invariant": "^2.2.4", "js-base64": "~3.7.2", "react-native-nitro-geolocation": "1.4.3", - "react-native-nitro-modules": "0.22.0", + "react-native-nitro-modules": "0.36.1", "react-native-permissions": "5.5.1", "react-native-geocoder": "0.5.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74f723a2f..c78e01966 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,10 +205,10 @@ importers: version: 0.5.0 react-native-nitro-geolocation: specifier: 1.4.3 - version: 1.4.3(react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + version: 1.4.3(react-native-nitro-modules@0.36.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) react-native-nitro-modules: - specifier: 0.22.0 - version: 0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + specifier: 0.36.1 + version: 0.36.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) react-native-permissions: specifier: 5.5.1 version: 5.5.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) @@ -6091,8 +6091,8 @@ packages: react-native: 0.84.1 react-native-nitro-modules: '*' - react-native-nitro-modules@0.22.0: - resolution: {integrity: sha512-3obSYG8dgg3D8lEU/xMKL/0r1A3FrKB2tIKa+LWfBaXnMfWwg5n1o2GRcLpbLtmgFPx903JjVuxhFDRAtslW0Q==} + react-native-nitro-modules@0.36.1: + resolution: {integrity: sha512-kBv/VvKqAmkXAvP1DxJMC9b/fRhh7JdSO4EUnPP46hJjrIFeFR8AwKm8mYaKZEuF014M/TVdv2vomVUW0umsQQ==} peerDependencies: react: 19.2.3 react-native: 0.84.1 @@ -13875,13 +13875,13 @@ snapshots: react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) react-native-animatable: 1.3.3 - react-native-nitro-geolocation@1.4.3(react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + react-native-nitro-geolocation@1.4.3(react-native-nitro-modules@0.36.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) - react-native-nitro-modules: 0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-nitro-modules: 0.36.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) - react-native-nitro-modules@0.22.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + react-native-nitro-modules@0.36.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) From 111feb451b47b55f15bb470f224d6b9d30d02969 Mon Sep 17 00:00:00 2001 From: stelselim Date: Wed, 22 Jul 2026 16:02:36 +0200 Subject: [PATCH 03/10] feat: refactor location handling functions --- .../src/geolocation/GetCurrentLocation.ts | 51 ++++++++++++++++- .../GetCurrentLocationMinimumAccuracy.ts | 56 ++++++++++++++++++- .../src/geolocation/utils.ts | 49 ---------------- 3 files changed, 102 insertions(+), 54 deletions(-) delete mode 100644 packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index e2691e192..73d714d60 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -5,9 +5,9 @@ // - the code between BEGIN USER CODE and END USER CODE // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. -import { getCurrentPosition } from "react-native-nitro-geolocation"; - -import { buildLocationOptions, mapPositionToMxObject } from "./utils"; +import { Big } from "big.js"; +import { Platform } from "react-native"; +import { getCurrentPosition, GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -51,5 +51,50 @@ export async function GetCurrentLocation( return Promise.reject(new Error(`Could not get current location: ${message}`)); } + function buildLocationOptions( + timeout: Big | undefined, + maximumAge: Big | undefined, + highAccuracy: boolean | undefined + ): LocationRequestOptions { + let timeoutNumber = timeout ? timeout.toNumber() : undefined; + const maximumAgeNumber = maximumAge ? maximumAge.toNumber() : undefined; + + // If the timeout is 0 or undefined (empty), it causes a crash on iOS. + // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) + // If the timeout is 0; we set timeout to 1 hour (no timeout) + if (Platform.OS === "ios") { + if (timeoutNumber === undefined) { + timeoutNumber = 30000; + } else if (timeoutNumber === 0) { + timeoutNumber = 3600000; + } + } + + return { + timeout: timeoutNumber, + maximumAge: maximumAgeNumber, + accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } + }; + } + function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { + mxObject.set("Timestamp", new Date(pos.timestamp)); + mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); + mxObject.set("Longitude", new Big(pos.coords.longitude.toFixed(8))); + mxObject.set("Accuracy", new Big(pos.coords.accuracy.toFixed(8))); + if (pos.coords.altitude != null) { + mxObject.set("Altitude", new Big(pos.coords.altitude.toFixed(8))); + } + if (pos.coords.altitudeAccuracy != null && pos.coords.altitudeAccuracy !== -1) { + mxObject.set("AltitudeAccuracy", new Big(pos.coords.altitudeAccuracy.toFixed(8))); + } + if (pos.coords.heading != null && pos.coords.heading !== -1) { + mxObject.set("Heading", new Big(pos.coords.heading.toFixed(8))); + } + if (pos.coords.speed != null && pos.coords.speed !== -1) { + mxObject.set("Speed", new Big(pos.coords.speed.toFixed(8))); + } + return mxObject; + } + // END USER CODE } diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts index 48fbd130d..ffc270b4d 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts @@ -5,8 +5,15 @@ // - the code between BEGIN USER CODE and END USER CODE // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. -import { watchPosition, unwatch, LocationError, GeolocationResponse } from "react-native-nitro-geolocation"; -import { buildLocationOptions, mapPositionToMxObject } from "./utils"; +import { Big } from "big.js"; +import { Platform } from "react-native"; +import { + watchPosition, + unwatch, + LocationError, + GeolocationResponse, + LocationRequestOptions +} from "react-native-nitro-geolocation"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -82,5 +89,50 @@ export async function GetCurrentLocationMinimumAccuracy( } }); + function buildLocationOptions( + timeout: Big | undefined, + maximumAge: Big | undefined, + highAccuracy: boolean | undefined + ): LocationRequestOptions { + let timeoutNumber = timeout ? timeout.toNumber() : undefined; + const maximumAgeNumber = maximumAge ? maximumAge.toNumber() : undefined; + + // If the timeout is 0 or undefined (empty), it causes a crash on iOS. + // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) + // If the timeout is 0; we set timeout to 1 hour (no timeout) + if (Platform.OS === "ios") { + if (timeoutNumber === undefined) { + timeoutNumber = 30000; + } else if (timeoutNumber === 0) { + timeoutNumber = 3600000; + } + } + + return { + timeout: timeoutNumber, + maximumAge: maximumAgeNumber, + accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } + }; + } + function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { + mxObject.set("Timestamp", new Date(pos.timestamp)); + mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); + mxObject.set("Longitude", new Big(pos.coords.longitude.toFixed(8))); + mxObject.set("Accuracy", new Big(pos.coords.accuracy.toFixed(8))); + if (pos.coords.altitude != null) { + mxObject.set("Altitude", new Big(pos.coords.altitude.toFixed(8))); + } + if (pos.coords.altitudeAccuracy != null && pos.coords.altitudeAccuracy !== -1) { + mxObject.set("AltitudeAccuracy", new Big(pos.coords.altitudeAccuracy.toFixed(8))); + } + if (pos.coords.heading != null && pos.coords.heading !== -1) { + mxObject.set("Heading", new Big(pos.coords.heading.toFixed(8))); + } + if (pos.coords.speed != null && pos.coords.speed !== -1) { + mxObject.set("Speed", new Big(pos.coords.speed.toFixed(8))); + } + return mxObject; + } + // END USER CODE } diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts deleted file mode 100644 index de6d142b8..000000000 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Big } from "big.js"; -import { Platform } from "react-native"; -import { GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; - -export function buildLocationOptions( - timeout: Big | undefined, - maximumAge: Big | undefined, - highAccuracy: boolean | undefined -): LocationRequestOptions { - let timeoutNumber = timeout ? timeout.toNumber() : undefined; - const maximumAgeNumber = maximumAge ? maximumAge.toNumber() : undefined; - - // If the timeout is 0 or undefined (empty), it causes a crash on iOS. - // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) - // If the timeout is 0; we set timeout to 1 hour (no timeout) - if (Platform.OS === "ios") { - if (timeoutNumber === undefined) { - timeoutNumber = 30000; - } else if (timeoutNumber === 0) { - timeoutNumber = 3600000; - } - } - - return { - timeout: timeoutNumber, - maximumAge: maximumAgeNumber, - accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } - }; -} - -export function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { - mxObject.set("Timestamp", new Date(pos.timestamp)); - mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); - mxObject.set("Longitude", new Big(pos.coords.longitude.toFixed(8))); - mxObject.set("Accuracy", new Big(pos.coords.accuracy.toFixed(8))); - if (pos.coords.altitude != null) { - mxObject.set("Altitude", new Big(pos.coords.altitude.toFixed(8))); - } - if (pos.coords.altitudeAccuracy != null && pos.coords.altitudeAccuracy !== -1) { - mxObject.set("AltitudeAccuracy", new Big(pos.coords.altitudeAccuracy.toFixed(8))); - } - if (pos.coords.heading != null && pos.coords.heading !== -1) { - mxObject.set("Heading", new Big(pos.coords.heading.toFixed(8))); - } - if (pos.coords.speed != null && pos.coords.speed !== -1) { - mxObject.set("Speed", new Big(pos.coords.speed.toFixed(8))); - } - return mxObject; -} From e15a8678b1d5b0633a9b82ff855b8454e986fa48 Mon Sep 17 00:00:00 2001 From: stelselim Date: Tue, 4 Aug 2026 13:28:26 +0200 Subject: [PATCH 04/10] feat: enhance GetCurrentLocation function to support geolocation in React Native and web --- .../src/geolocation/GetCurrentLocation.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index 73d714d60..9fafcf33a 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -33,9 +33,33 @@ export async function GetCurrentLocation( ): Promise { // BEGIN USER CODE + const isReactNative = navigator && navigator.product === "ReactNative"; + const isWeb = navigator && navigator.geolocation; + + if (!isReactNative && !isWeb) { + return Promise.reject(new Error("Geolocation module could not be found")); + } + const options = buildLocationOptions(timeout, maximumAge, highAccuracy); try { - const position = await getCurrentPosition(options); + let position: GeolocationResponse; + + if (isReactNative) { + position = await getCurrentPosition(options); + } else { + position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + pos => resolve(pos as unknown as GeolocationResponse), + err => reject(err), + { + timeout: options.timeout ?? undefined, + maximumAge: options.maximumAge ?? undefined, + enableHighAccuracy: highAccuracy ?? false + } + ); + }); + } + return new Promise((resolve, reject) => { mx.data.create({ entity: "NanoflowCommons.Geolocation", From 45b095926b7d3ee99cf0e513af11d500e1210247 Mon Sep 17 00:00:00 2001 From: stelselim Date: Tue, 4 Aug 2026 13:37:56 +0200 Subject: [PATCH 05/10] feat: enhance GetCurrentLocation to return detailed geolocation data --- .../src/geolocation/GetCurrentLocation.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index 9fafcf33a..14034dead 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -49,7 +49,19 @@ export async function GetCurrentLocation( } else { position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( - pos => resolve(pos as unknown as GeolocationResponse), + pos => + resolve({ + coords: { + latitude: pos.coords.latitude, + longitude: pos.coords.longitude, + altitude: pos.coords.altitude ?? null, + accuracy: pos.coords.accuracy, + altitudeAccuracy: pos.coords.altitudeAccuracy ?? null, + heading: pos.coords.heading ?? null, + speed: pos.coords.speed ?? null + }, + timestamp: pos.timestamp + }), err => reject(err), { timeout: options.timeout ?? undefined, From e45d8c5869300338066ea4abc71e6afeaec3c002 Mon Sep 17 00:00:00 2001 From: stelselim Date: Tue, 4 Aug 2026 13:45:14 +0200 Subject: [PATCH 06/10] feat: refactor GetCurrentLocation to normalize web position handling --- .../src/geolocation/GetCurrentLocation.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index 14034dead..5fb381a3c 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -49,19 +49,7 @@ export async function GetCurrentLocation( } else { position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( - pos => - resolve({ - coords: { - latitude: pos.coords.latitude, - longitude: pos.coords.longitude, - altitude: pos.coords.altitude ?? null, - accuracy: pos.coords.accuracy, - altitudeAccuracy: pos.coords.altitudeAccuracy ?? null, - heading: pos.coords.heading ?? null, - speed: pos.coords.speed ?? null - }, - timestamp: pos.timestamp - }), + pos => resolve(normalizeWebPosition(pos)), err => reject(err), { timeout: options.timeout ?? undefined, @@ -82,8 +70,8 @@ export async function GetCurrentLocation( reject(new Error("Could not create 'NanoflowCommons.Geolocation' object to store location")) }); }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); + } catch (error: any) { + const message = error instanceof Error ? error.message : error?.message ?? String(error); return Promise.reject(new Error(`Could not get current location: ${message}`)); } @@ -112,6 +100,22 @@ export async function GetCurrentLocation( accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } }; } + + function normalizeWebPosition(pos: GeolocationPosition): GeolocationResponse { + return { + coords: { + latitude: pos.coords.latitude, + longitude: pos.coords.longitude, + altitude: pos.coords.altitude ?? null, + accuracy: pos.coords.accuracy, + altitudeAccuracy: pos.coords.altitudeAccuracy ?? null, + heading: pos.coords.heading ?? null, + speed: pos.coords.speed ?? null + }, + timestamp: pos.timestamp + }; + } + function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { mxObject.set("Timestamp", new Date(pos.timestamp)); mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); From 2e8d89783e5074462a2f864a70af15dfe39938ed Mon Sep 17 00:00:00 2001 From: stelselim Date: Tue, 4 Aug 2026 14:14:15 +0200 Subject: [PATCH 07/10] feat: enhance GetCurrentLocationMinimumAccuracy to support web geolocation --- .../GetCurrentLocationMinimumAccuracy.ts | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts index ffc270b4d..1728548af 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts @@ -7,13 +7,7 @@ // Other code you write will be lost the next time you deploy the project. import { Big } from "big.js"; import { Platform } from "react-native"; -import { - watchPosition, - unwatch, - LocationError, - GeolocationResponse, - LocationRequestOptions -} from "react-native-nitro-geolocation"; +import { watchPosition, unwatch, GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -43,16 +37,47 @@ export async function GetCurrentLocationMinimumAccuracy( ): Promise { // BEGIN USER CODE + const isReactNative = navigator && navigator.product === "ReactNative"; + const isWeb = navigator && navigator.geolocation; + + if (!isReactNative && !isWeb) { + return Promise.reject(new Error("Geolocation module could not be found")); + } + return new Promise((resolve, reject) => { const options = buildLocationOptions(timeout, maximumAge, highAccuracy); let lastAccruedPosition: GeolocationResponse | undefined; const timeoutMs = timeout ? timeout.toNumber() : 30000; const timeoutId = setTimeout(onTimeout, timeoutMs); - const token = watchPosition(onSuccess, onError, options); + + let clearWatch: () => void; + + if (isReactNative) { + const token = watchPosition(onSuccess, onError, options); + clearWatch = () => unwatch(token); + } else { + // Workaround: browsers may ignore maximumAge on watchPosition unless getCurrentPosition is called first. + // https://stackoverflow.com/questions/3397585/navigator-geolocation-getcurrentposition-sometimes-works-sometimes-doesnt + navigator.geolocation.getCurrentPosition( + () => {}, + () => {}, + {} + ); + const watchId = navigator.geolocation.watchPosition( + pos => onSuccess(normalizeWebPosition(pos)), + err => onError({ code: err.code, message: err.message }), + { + timeout: options.timeout ?? undefined, + maximumAge: options.maximumAge ?? undefined, + enableHighAccuracy: highAccuracy ?? false + } + ); + clearWatch = () => navigator.geolocation.clearWatch(watchId); + } function onTimeout(): void { - unwatch(token); + clearWatch(); if (lastAccruedPosition) { createGeolocationObject(lastAccruedPosition); @@ -64,7 +89,7 @@ export async function GetCurrentLocationMinimumAccuracy( function onSuccess(position: GeolocationResponse): void { if (!minimumAccuracy || Number(minimumAccuracy) >= position.coords.accuracy) { clearTimeout(timeoutId); - unwatch(token); + clearWatch(); createGeolocationObject(position); } else { if (!lastAccruedPosition || position.coords.accuracy < lastAccruedPosition.coords.accuracy) { @@ -73,9 +98,9 @@ export async function GetCurrentLocationMinimumAccuracy( } } - function onError(error: LocationError): void { + function onError(error: { code: number; message: string }): void { clearTimeout(timeoutId); - unwatch(token); + clearWatch(); reject(new Error(error.message)); } @@ -114,6 +139,21 @@ export async function GetCurrentLocationMinimumAccuracy( accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } }; } + function normalizeWebPosition(pos: GeolocationPosition): GeolocationResponse { + return { + coords: { + latitude: pos.coords.latitude, + longitude: pos.coords.longitude, + altitude: pos.coords.altitude ?? null, + accuracy: pos.coords.accuracy, + altitudeAccuracy: pos.coords.altitudeAccuracy ?? null, + heading: pos.coords.heading ?? null, + speed: pos.coords.speed ?? null + }, + timestamp: pos.timestamp + }; + } + function mapPositionToMxObject(mxObject: mendix.lib.MxObject, pos: GeolocationResponse): mendix.lib.MxObject { mxObject.set("Timestamp", new Date(pos.timestamp)); mxObject.set("Latitude", new Big(pos.coords.latitude.toFixed(8))); From c29f3978f2b4565020be479aa68a520d0840158e Mon Sep 17 00:00:00 2001 From: stelselim Date: Thu, 6 Aug 2026 11:56:48 +0200 Subject: [PATCH 08/10] fix: address review feedback on geolocation nitro migration - Fix timeout=0 watchdog firing immediately in GetCurrentLocationMinimumAccuracy by deriving the watchdog from a single clamped timeout value and lifting the 0/undefined clamp out of the iOS-only branch - Make Platform lazy via require("react-native") in GetCurrentLocation; remove now-unused Platform import in GetCurrentLocationMinimumAccuracy - Keep manual web branch with an explanatory comment - Remove redundant '?? undefined' and simplify error message narrowing - Note that normalizeWebPosition is duplicated across both actions - Revert stray apostrophe change outside BEGIN USER CODE - Remove unused invariant dependency and its rollup copy step - Document why jsx is required in rollup config - Bump version to 7.3.0 and add CHANGELOG entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configs/jsactions/rollup.config.mjs | 14 +++----- .../nanoflow-actions-native/CHANGELOG.md | 10 ++++++ .../nanoflow-actions-native/package.json | 3 +- .../src/geolocation/GetCurrentLocation.ts | 15 ++++++--- .../GetCurrentLocationMinimumAccuracy.ts | 33 +++++++++++-------- pnpm-lock.yaml | 3 -- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/configs/jsactions/rollup.config.mjs b/configs/jsactions/rollup.config.mjs index 115f7c3db..29e99997c 100644 --- a/configs/jsactions/rollup.config.mjs +++ b/configs/jsactions/rollup.config.mjs @@ -35,6 +35,10 @@ export default async args => { allowSyntheticDefaultImports: true, compilerOptions: { newLine: "CRLF", + // `react-native-nitro-geolocation` ships no compiled JS; its entry points are `.tsx` + // (`main: "src/index"`, `browser: "src/index.web.tsx"`). The TS plugin therefore needs + // `jsx` set to parse those `.tsx` files, otherwise the build fails with + // `TS6142: Module ... was resolved to '.../src/index.tsx', but '--jsx' is not set`. jsx: "react-native" } }); @@ -93,16 +97,6 @@ export default async args => { overwrite: true } ); - } else if (args.configProject === "nanoflowcommons") { - // `invariant` is being used silently by @react-native-community/geolocation; it is not listed as a dependency nor peerDependency. - // https://github.dev/react-native-geolocation/react-native-geolocation/blob/1786929f2be581da91082ff857c2393da5e597b3/js/implementation.native.js#L13 - await copyAsync( - dirname(require.resolve("invariant")), - join(outDir, "node_modules", "invariant"), - { - overwrite: true - } - ); } // this is helpful to copy the files and folders to a test project path for dev/testing purposes. diff --git a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md index 1d085bb9d..bbae8f75e 100644 --- a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md +++ b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fixed an issue where base64-to-image decoding was failing. +## [7.3.0] Nanoflow Commons - 2026-8-6 + +### Changed + +- Migrated the geolocation actions from `@react-native-community/geolocation` to `react-native-nitro-geolocation`. + +### Fixed + +- Fixed a resource leak in `Get current location with minimum accuracy` where the timeout and location watcher were not cleared on error. + ## [7.2.0] Nanoflow Commons - 2026-7-3 - Close offline database connection before navigating between pages with OpenURL nanoflow action. diff --git a/packages/jsActions/nanoflow-actions-native/package.json b/packages/jsActions/nanoflow-actions-native/package.json index bfcd77854..7ab3bbeec 100644 --- a/packages/jsActions/nanoflow-actions-native/package.json +++ b/packages/jsActions/nanoflow-actions-native/package.json @@ -1,7 +1,7 @@ { "name": "nanoflow-actions-native", "moduleName": "Nanoflow Commons", - "version": "7.2.0", + "version": "7.3.0", "license": "Apache-2.0", "copyright": "© Mendix Technology BV 2022. All rights reserved.", "repository": { @@ -27,7 +27,6 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "2.2.0", - "invariant": "^2.2.4", "js-base64": "~3.7.2", "react-native-nitro-geolocation": "1.4.3", "react-native-nitro-modules": "0.36.1", diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts index 5fb381a3c..d9ca95e21 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocation.ts @@ -6,7 +6,6 @@ // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. import { Big } from "big.js"; -import { Platform } from "react-native"; import { getCurrentPosition, GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; // BEGIN EXTRA CODE @@ -40,6 +39,9 @@ export async function GetCurrentLocation( return Promise.reject(new Error("Geolocation module could not be found")); } + // We keep a manual web branch (backed by `navigator.geolocation`) rather than relying on the + // library's web entry, because it is not guaranteed that the Mendix web client resolves the + // package's `browser`/`exports` condition. If that is ever confirmed, this branch can be dropped. const options = buildLocationOptions(timeout, maximumAge, highAccuracy); try { let position: GeolocationResponse; @@ -52,8 +54,8 @@ export async function GetCurrentLocation( pos => resolve(normalizeWebPosition(pos)), err => reject(err), { - timeout: options.timeout ?? undefined, - maximumAge: options.maximumAge ?? undefined, + timeout: options.timeout, + maximumAge: options.maximumAge, enableHighAccuracy: highAccuracy ?? false } ); @@ -71,7 +73,7 @@ export async function GetCurrentLocation( }); }); } catch (error: any) { - const message = error instanceof Error ? error.message : error?.message ?? String(error); + const message = error?.message ?? String(error); return Promise.reject(new Error(`Could not get current location: ${message}`)); } @@ -86,7 +88,7 @@ export async function GetCurrentLocation( // If the timeout is 0 or undefined (empty), it causes a crash on iOS. // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) // If the timeout is 0; we set timeout to 1 hour (no timeout) - if (Platform.OS === "ios") { + if (isReactNative && require("react-native").Platform.OS === "ios") { if (timeoutNumber === undefined) { timeoutNumber = 30000; } else if (timeoutNumber === 0) { @@ -101,6 +103,9 @@ export async function GetCurrentLocation( }; } + // NOTE: `normalizeWebPosition` is duplicated verbatim in `GetCurrentLocationMinimumAccuracy.ts`. + // The Mendix action model does not allow sharing code across action files, so if you change this + // function, keep the copy in the other action in sync. function normalizeWebPosition(pos: GeolocationPosition): GeolocationResponse { return { coords: { diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts index 1728548af..5c799012f 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts @@ -6,7 +6,6 @@ // - the code between BEGIN EXTRA CODE and END EXTRA CODE // Other code you write will be lost the next time you deploy the project. import { Big } from "big.js"; -import { Platform } from "react-native"; import { watchPosition, unwatch, GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; // BEGIN EXTRA CODE @@ -19,7 +18,7 @@ import { watchPosition, unwatch, GeolocationResponse, LocationRequestOptions } f * * On hybrid and native platforms the permission should be requested with the `RequestLocationPermission` action. * - * For good user experience, disable the nanoflow during action using property `Disabled during action` if you're using `Call a nanoflow button` to run JS Action `Get current location with minimum accuracy`. + * For good user experience, disable the nanoflow during action using property `Disabled during action` if you’re using `Call a nanoflow button` to run JS Action `Get current location with minimum accuracy`. * * Best practices: * https://developers.google.com/web/fundamentals/native-hardware/user-location/ @@ -48,7 +47,10 @@ export async function GetCurrentLocationMinimumAccuracy( const options = buildLocationOptions(timeout, maximumAge, highAccuracy); let lastAccruedPosition: GeolocationResponse | undefined; - const timeoutMs = timeout ? timeout.toNumber() : 30000; + // Derive the watchdog timeout from the same clamped value used for the location layer, + // so the two never disagree. `new Big(0)` is truthy, so we must not test `timeout` + // directly here or a configured `0` would fire the watchdog on the next tick. + const timeoutMs = options.timeout ?? 30000; const timeoutId = setTimeout(onTimeout, timeoutMs); let clearWatch: () => void; @@ -68,8 +70,8 @@ export async function GetCurrentLocationMinimumAccuracy( pos => onSuccess(normalizeWebPosition(pos)), err => onError({ code: err.code, message: err.message }), { - timeout: options.timeout ?? undefined, - maximumAge: options.maximumAge ?? undefined, + timeout: options.timeout, + maximumAge: options.maximumAge, enableHighAccuracy: highAccuracy ?? false } ); @@ -122,15 +124,15 @@ export async function GetCurrentLocationMinimumAccuracy( let timeoutNumber = timeout ? timeout.toNumber() : undefined; const maximumAgeNumber = maximumAge ? maximumAge.toNumber() : undefined; - // If the timeout is 0 or undefined (empty), it causes a crash on iOS. - // If the timeout is undefined (empty); we set timeout to 30 sec (default timeout) - // If the timeout is 0; we set timeout to 1 hour (no timeout) - if (Platform.OS === "ios") { - if (timeoutNumber === undefined) { - timeoutNumber = 30000; - } else if (timeoutNumber === 0) { - timeoutNumber = 3600000; - } + // Normalize the timeout so the watchdog and the location layer always agree, on every platform: + // - undefined (empty) -> 30 sec (default timeout) + // - 0 -> 1 hour (treated as "no timeout") + // A timeout of 0 or undefined also crashes on iOS, and on web `timeout: 0` means + // "fail immediately with TIMEOUT" on every update, so it must never reach the location layer. + if (timeoutNumber === undefined) { + timeoutNumber = 30000; + } else if (timeoutNumber === 0) { + timeoutNumber = 3600000; } return { @@ -139,6 +141,9 @@ export async function GetCurrentLocationMinimumAccuracy( accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } }; } + // NOTE: `normalizeWebPosition` is duplicated verbatim in `GetCurrentLocation.ts`. + // The Mendix action model does not allow sharing code across action files, so if you change this + // function, keep the copy in the other action in sync. function normalizeWebPosition(pos: GeolocationPosition): GeolocationResponse { return { coords: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c78e01966..a30f70033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,9 +194,6 @@ importers: '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3)) - invariant: - specifier: ^2.2.4 - version: 2.2.4 js-base64: specifier: ~3.7.2 version: 3.7.8 From aed6f9db9ca49d1cdd43e6cd74bf8d87249c8b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selim=20=C3=9Cstel?= <46201537+stelselim@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:44:25 +0200 Subject: [PATCH 09/10] Update CHANGELOG.md for geolocation migration Removed version 7.3.0 section and updated unreleased notes. --- packages/jsActions/nanoflow-actions-native/CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md index bbae8f75e..e937f87a7 100644 --- a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md +++ b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md @@ -7,11 +7,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] - Fixed an issue where base64-to-image decoding was failing. - -## [7.3.0] Nanoflow Commons - 2026-8-6 - -### Changed - - Migrated the geolocation actions from `@react-native-community/geolocation` to `react-native-nitro-geolocation`. ### Fixed From b1369b1afeedb79f39ca012d3531244c2184cbc4 Mon Sep 17 00:00:00 2001 From: stelselim Date: Thu, 6 Aug 2026 12:46:27 +0200 Subject: [PATCH 10/10] feat: fall back to best-so-far fix on error in GetCurrentLocationMinimumAccuracy Address review design question: onError now returns the best accrued position (if any) instead of rejecting on a transient error such as POSITION_UNAVAILABLE, matching the onTimeout fallback and the action's best-effort-within-a-timeout intent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../geolocation/GetCurrentLocationMinimumAccuracy.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts index 5c799012f..b1419bf0e 100644 --- a/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts +++ b/packages/jsActions/nanoflow-actions-native/src/geolocation/GetCurrentLocationMinimumAccuracy.ts @@ -103,7 +103,15 @@ export async function GetCurrentLocationMinimumAccuracy( function onError(error: { code: number; message: string }): void { clearTimeout(timeoutId); clearWatch(); - reject(new Error(error.message)); + + // Best effort within a timeout: if we already captured a usable (though not yet + // minimum-accuracy) fix, return it instead of failing on a transient error such as + // POSITION_UNAVAILABLE. Mirrors the onTimeout fallback. + if (lastAccruedPosition) { + createGeolocationObject(lastAccruedPosition); + } else { + reject(new Error(error.message)); + } } function createGeolocationObject(position: GeolocationResponse): void {