-
Notifications
You must be signed in to change notification settings - Fork 22
[MOO-2225] migrate geolocation to react native nitro geolocation #552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
70b1907
c34370b
111feb4
e15a867
45b0959
e45d8c5
2e8d897
c29f397
aed6f9d
b1369b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,7 @@ | |
| // - 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 { getCurrentPosition, GeolocationResponse, LocationRequestOptions } from "react-native-nitro-geolocation"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good news — I think we may be able to drop the hand-rolled web branch entirely here. The library ships web support for the Modern API root import: browser builds resolve to One thing worth verifying before we rely on it: whether the Mendix web client resolves the package's |
||
|
|
||
| // BEGIN EXTRA CODE | ||
| // END EXTRA CODE | ||
|
|
@@ -39,94 +32,114 @@ export async function GetCurrentLocation( | |
| ): Promise<mendix.lib.MxObject> { | ||
| // BEGIN USER CODE | ||
|
|
||
| let reactNativeModule: { NativeModules: typeof NativeModules; Platform: typeof Platform } | undefined; | ||
| let geolocationModule: typeof import("@react-native-community/geolocation").default | Geolocation; | ||
| const isReactNative = navigator && navigator.product === "ReactNative"; | ||
| const isWeb = navigator && navigator.geolocation; | ||
|
|
||
| if (navigator && navigator.product === "ReactNative") { | ||
| reactNativeModule = require("react-native"); | ||
| if (!isReactNative && !isWeb) { | ||
| return Promise.reject(new Error("Geolocation module could not be found")); | ||
| } | ||
|
|
||
| if (!reactNativeModule) { | ||
| return Promise.reject(new Error("React Native 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; | ||
|
|
||
| if (reactNativeModule.NativeModules.RNFusedLocation) { | ||
| geolocationModule = (await import("@react-native-community/geolocation")).default; | ||
| } else if (reactNativeModule.NativeModules.RNCGeolocation) { | ||
| geolocationModule = Geolocation; | ||
| if (isReactNative) { | ||
| position = await getCurrentPosition(options); | ||
| } else { | ||
| return Promise.reject(new Error("Geolocation module could not be found")); | ||
| position = await new Promise<GeolocationResponse>((resolve, reject) => { | ||
| navigator.geolocation.getCurrentPosition( | ||
| pos => resolve(normalizeWebPosition(pos)), | ||
| err => reject(err), | ||
| { | ||
| timeout: options.timeout, | ||
| maximumAge: options.maximumAge, | ||
| enableHighAccuracy: highAccuracy ?? false | ||
| } | ||
| ); | ||
| }); | ||
| } | ||
| } else if (navigator && navigator.geolocation) { | ||
| geolocationModule = navigator.geolocation; | ||
|
vadymv-mendix marked this conversation as resolved.
|
||
| } 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 { | ||
| 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")) | ||
| }); | ||
| } | ||
| }); | ||
| } catch (error: any) { | ||
| const message = error?.message ?? String(error); | ||
| 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; | ||
|
|
||
| function onError(error: GeolocationError | GeoError): void { | ||
| return reject(new Error(error.message)); | ||
| // 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 (isReactNative && require("react-native").Platform.OS === "ios") { | ||
| if (timeoutNumber === undefined) { | ||
| timeoutNumber = 30000; | ||
| } else if (timeoutNumber === 0) { | ||
| timeoutNumber = 3600000; | ||
| } | ||
| } | ||
|
|
||
| function getOptions(): GeolocationOptions | GeoOptions { | ||
| let timeoutNumber = timeout && Number(timeout.toString()); | ||
| const maximumAgeNumber = maximumAge && Number(maximumAge.toString()); | ||
| return { | ||
| timeout: timeoutNumber, | ||
| maximumAge: maximumAgeNumber, | ||
| accuracy: highAccuracy ? { android: "high", ios: "best" } : { android: "balanced", ios: "hundredMeters" } | ||
| }; | ||
| } | ||
|
|
||
| // 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; | ||
| } | ||
| } | ||
| // 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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If we end up using the library's built-in web support, this can be removed outright. If we keep the manual path, could we add a short note that the two copies need to stay in sync? Sharing across two action files isn't really possible in the Mendix model as far as I know, so a comment is probably the most we can do. |
||
| 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 | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| timeout: timeoutNumber, | ||
| maximumAge: maximumAgeNumber, | ||
| enableHighAccuracy: highAccuracy | ||
| }; | ||
| 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))); | ||
| } | ||
|
|
||
| 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; | ||
| 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 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth documenting why this is needed, since it's non-obvious.
react-native-nitro-geolocationships no compiled JS at all — the tarball has 0.jsfiles and 54.ts/.tsx, withmain: "src/index"andbrowser: "src/index.web.tsx"— so the TS plugin needsjsxset in order to parse the.tsxentry point. I confirmed it's load-bearing: without it the build fails withTS6142: Module ... was resolved to '.../src/index.tsx', but '--jsx' is not set.The flag itself is fine. The thing I'd want confirmed before merge is the consequence: the client now resolves raw TSX at runtime for this dependency, where the old library shipped
lib/commonjs+lib/module. That's worth a quick sanity check in a real app (web especially), independent of the web-support question onGetCurrentLocation.ts.Could you add a short comment here noting why
jsxis required? Otherwise it looks unrelated to a geolocation migration and someone may remove it later.