@trackunit/react-core-hooks 1.12.67 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -230,6 +230,99 @@ const useFilterBarContext = () => {
230
230
  return context;
231
231
  };
232
232
 
233
+ const useStandaloneGeolocation = ({ enabled, requestOnMount, }) => {
234
+ const [position, setPosition] = react.useState(null);
235
+ const queryGeolocationPermission = react.useCallback(async () => {
236
+ if (!enabled || typeof navigator === "undefined") {
237
+ return null;
238
+ }
239
+ try {
240
+ const status = await navigator.permissions.query({ name: "geolocation" });
241
+ return status.state;
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }, [enabled]);
247
+ const getCurrentPosition = react.useCallback(async () => {
248
+ if (!enabled || typeof navigator === "undefined") {
249
+ return null;
250
+ }
251
+ return await new Promise(resolve => {
252
+ try {
253
+ navigator.geolocation.getCurrentPosition(currentPosition => {
254
+ resolve([currentPosition.coords.longitude, currentPosition.coords.latitude]);
255
+ }, () => {
256
+ resolve(null);
257
+ });
258
+ }
259
+ catch {
260
+ resolve(null);
261
+ }
262
+ });
263
+ }, [enabled]);
264
+ const getPosition = react.useCallback(async (options) => {
265
+ const prompt = options?.prompt !== false;
266
+ if (!prompt) {
267
+ const permissionState = await queryGeolocationPermission();
268
+ if (permissionState !== "granted") {
269
+ return null;
270
+ }
271
+ }
272
+ const currentPosition = await getCurrentPosition();
273
+ if (currentPosition !== null) {
274
+ setPosition(currentPosition);
275
+ }
276
+ return currentPosition;
277
+ }, [getCurrentPosition, queryGeolocationPermission]);
278
+ react.useEffect(() => {
279
+ if (!enabled || requestOnMount !== true) {
280
+ return;
281
+ }
282
+ void queryGeolocationPermission()
283
+ .then(permissionState => {
284
+ if (permissionState !== "granted") {
285
+ return null;
286
+ }
287
+ return getCurrentPosition();
288
+ })
289
+ .then(currentPosition => {
290
+ if (currentPosition !== null) {
291
+ setPosition(currentPosition);
292
+ }
293
+ });
294
+ }, [enabled, getCurrentPosition, queryGeolocationPermission, requestOnMount]);
295
+ return react.useMemo(() => ({
296
+ position,
297
+ getPosition,
298
+ }), [getPosition, position]);
299
+ };
300
+ /**
301
+ * Hook providing geolocation capabilities.
302
+ *
303
+ * In the host, geolocation is resolved directly via the browser API.
304
+ * In Iris Apps, requests are proxied to the host via the iframe bridge,
305
+ * so permission is only requested once at the host level.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const { position, getPosition } = useGeolocation();
310
+ *
311
+ * // User-initiated request (e.g. "My Location" button)
312
+ * onClick: () => {
313
+ * void getPosition().then(pos => { if (pos) fitBounds(pos); });
314
+ * }
315
+ * ```
316
+ */
317
+ const useGeolocation = (options) => {
318
+ const context = react.useContext(reactCoreContextsApi.GeolocationContext);
319
+ const standaloneGeolocation = useStandaloneGeolocation({
320
+ enabled: context === null,
321
+ requestOnMount: options?.requestOnMount,
322
+ });
323
+ return context ?? standaloneGeolocation;
324
+ };
325
+
233
326
  /**
234
327
  * This is a hook to use the TokenContext.
235
328
  *
@@ -396,7 +489,7 @@ const resolveAccess = async (context, options) => {
396
489
  }
397
490
  return context.hasAccessTo(options);
398
491
  };
399
- const INITIAL_STATE = {
492
+ const INITIAL_STATE$1 = {
400
493
  status: "loading",
401
494
  hasAccess: undefined,
402
495
  error: undefined,
@@ -404,7 +497,7 @@ const INITIAL_STATE = {
404
497
  const accessReducer = (_state, action) => {
405
498
  switch (action.type) {
406
499
  case "FETCH_START":
407
- return INITIAL_STATE;
500
+ return INITIAL_STATE$1;
408
501
  case "FETCH_SUCCESS":
409
502
  return { status: "success", hasAccess: action.hasAccess, error: undefined };
410
503
  case "FETCH_ERROR":
@@ -427,7 +520,7 @@ const accessReducer = (_state, action) => {
427
520
  const useHasAccessTo = (options) => {
428
521
  const context = react.useContext(reactCoreContextsApi.NavigationContext);
429
522
  const [stableOptions, setStableOptions] = react.useState(options);
430
- const [state, dispatch] = react.useReducer(accessReducer, INITIAL_STATE);
523
+ const [state, dispatch] = react.useReducer(accessReducer, INITIAL_STATE$1);
431
524
  if (!esToolkit.isEqual(stableOptions, options)) {
432
525
  setStableOptions(options);
433
526
  }
@@ -576,6 +669,17 @@ const useCustomerRuntime = () => {
576
669
  return { customerInfo, loading, error };
577
670
  };
578
671
 
672
+ const eventRuntimeReducer = (_state, action) => {
673
+ switch (action.type) {
674
+ case "success":
675
+ return { status: "success", eventInfo: action.eventInfo };
676
+ case "error":
677
+ return { status: "error", error: action.error };
678
+ default:
679
+ return _state;
680
+ }
681
+ };
682
+ const INITIAL_STATE = { status: "loading" };
579
683
  /**
580
684
  * A hook to expose event runtime for React components
581
685
  *
@@ -594,25 +698,17 @@ const useCustomerRuntime = () => {
594
698
  * }, [getEventQuery, eventInfo]);
595
699
  */
596
700
  const useEventRuntime = () => {
597
- const [eventInfo, setEventInfo] = react.useState();
598
- const [loading, setLoading] = react.useState(true);
599
- const [error, setError] = react.useState();
701
+ const [state, dispatch] = react.useReducer(eventRuntimeReducer, INITIAL_STATE);
600
702
  react.useEffect(() => {
601
- const getEventInfo = async () => {
602
- setLoading(true);
603
- try {
604
- const updatedEventInfo = await irisAppRuntimeCore.EventRuntime.getEventInfo();
605
- setLoading(false);
606
- setEventInfo(updatedEventInfo);
607
- }
608
- catch (e) {
609
- setLoading(false);
610
- setError(new Error("Failed to get event info"));
611
- }
612
- };
613
- void getEventInfo();
703
+ void irisAppRuntimeCore.EventRuntime.getEventInfo()
704
+ .then(eventInfo => dispatch({ type: "success", eventInfo }))
705
+ .catch(() => dispatch({ type: "error", error: new Error("Failed to get event info") }));
614
706
  }, []);
615
- return { eventInfo, loading, error };
707
+ return {
708
+ eventInfo: state.status === "success" ? state.eventInfo : undefined,
709
+ loading: state.status === "loading",
710
+ error: state.status === "error" ? state.error : undefined,
711
+ };
616
712
  };
617
713
 
618
714
  /**
@@ -981,6 +1077,28 @@ const useWidgetConfigAsync = () => {
981
1077
  }
982
1078
  return context;
983
1079
  };
1080
+ const widgetDataReducer = (state, action) => {
1081
+ switch (action.type) {
1082
+ case "dataLoaded":
1083
+ return { ...state, data: action.data, loadingData: false };
1084
+ case "dataVersionLoaded":
1085
+ return { ...state, dataVersion: action.dataVersion };
1086
+ case "titleLoaded":
1087
+ return { ...state, title: action.title };
1088
+ case "dataUpdated":
1089
+ return { ...state, data: action.data, dataVersion: action.dataVersion };
1090
+ case "titleUpdated":
1091
+ return { ...state, title: action.title };
1092
+ default:
1093
+ return state;
1094
+ }
1095
+ };
1096
+ const INITIAL_WIDGET_DATA_STATE = {
1097
+ data: null,
1098
+ loadingData: true,
1099
+ dataVersion: null,
1100
+ title: null,
1101
+ };
984
1102
  /**
985
1103
  * This is a hook to use the WidgetConfigContext.
986
1104
  *
@@ -996,13 +1114,9 @@ const useWidgetConfigAsync = () => {
996
1114
  */
997
1115
  const useWidgetConfig = () => {
998
1116
  const widgetConfigContext = useWidgetConfigAsync();
999
- const [data, setData] = react.useState(null);
1000
- const [loadingData, setLoadingData] = react.useState(true);
1001
- const [dataVersion, setDataVersion] = react.useState(null);
1002
- const [title, setTitle] = react.useState(null);
1117
+ const [widgetData, dispatch] = react.useReducer(widgetDataReducer, INITIAL_WIDGET_DATA_STATE);
1003
1118
  const filters = useFilterBarContext();
1004
1119
  const { timeRange } = useTimeRange();
1005
- // use window.location.hash directly to avoid depending on tanstack router in core-hooks
1006
1120
  const [edit, setEdit] = react.useState(() => window.location.hash.includes("edit=true"));
1007
1121
  react.useEffect(() => {
1008
1122
  const handleHashChange = () => {
@@ -1013,29 +1127,27 @@ const useWidgetConfig = () => {
1013
1127
  }, []);
1014
1128
  const widgetConfigContextRef = react.useRef(widgetConfigContext);
1015
1129
  react.useEffect(() => {
1016
- void widgetConfigContextRef.current.getData().then(d => {
1017
- setData(d);
1018
- setLoadingData(false);
1019
- });
1020
- void widgetConfigContextRef.current.getDataVersion().then(dv => setDataVersion(dv));
1021
- void widgetConfigContextRef.current.getTitle().then(t => setTitle(t));
1130
+ void widgetConfigContextRef.current.getData().then(d => dispatch({ type: "dataLoaded", data: d }));
1131
+ void widgetConfigContextRef.current
1132
+ .getDataVersion()
1133
+ .then(dv => dispatch({ type: "dataVersionLoaded", dataVersion: dv }));
1134
+ void widgetConfigContextRef.current.getTitle().then(t => dispatch({ type: "titleLoaded", title: t }));
1022
1135
  }, []);
1023
1136
  const result = react.useMemo(() => ({
1024
- data,
1137
+ data: widgetData.data,
1025
1138
  setData: async (newData, newDataVersion) => {
1026
1139
  await widgetConfigContext.setData(newData, newDataVersion);
1027
- setData(newData);
1028
- setDataVersion(newDataVersion);
1140
+ dispatch({ type: "dataUpdated", data: newData, dataVersion: newDataVersion });
1029
1141
  },
1030
- dataVersion,
1031
- loadingData,
1142
+ dataVersion: widgetData.dataVersion,
1143
+ loadingData: widgetData.loadingData,
1032
1144
  setLoadingState: async (newLoadingState) => {
1033
1145
  await widgetConfigContext.setLoadingState(newLoadingState);
1034
1146
  },
1035
- title,
1147
+ title: widgetData.title,
1036
1148
  setTitle: async (newTitle) => {
1037
1149
  await widgetConfigContext.setTitle(newTitle);
1038
- setTitle(newTitle);
1150
+ dispatch({ type: "titleUpdated", title: newTitle });
1039
1151
  },
1040
1152
  filters,
1041
1153
  timeRange,
@@ -1048,13 +1160,25 @@ const useWidgetConfig = () => {
1048
1160
  if (props) {
1049
1161
  if (props.newData && props.newData.data) {
1050
1162
  await irisAppRuntimeCore.WidgetConfigRuntime.setWidgetData(props.newData.data, props.newData.dataVersion ?? 1, props.newTitle ?? undefined);
1051
- setData(props.newData.data);
1052
- setDataVersion(props.newData.dataVersion ?? 1);
1163
+ dispatch({
1164
+ type: "dataUpdated",
1165
+ data: props.newData.data,
1166
+ dataVersion: props.newData.dataVersion ?? 1,
1167
+ });
1053
1168
  }
1054
1169
  }
1055
1170
  await widgetConfigContext.closeEditMode();
1056
1171
  },
1057
- }), [data, dataVersion, title, filters, timeRange, edit, loadingData, widgetConfigContext]);
1172
+ }), [
1173
+ widgetData.data,
1174
+ widgetData.dataVersion,
1175
+ widgetData.title,
1176
+ filters,
1177
+ timeRange,
1178
+ edit,
1179
+ widgetData.loadingData,
1180
+ widgetConfigContext,
1181
+ ]);
1058
1182
  return result;
1059
1183
  };
1060
1184
 
@@ -1078,6 +1202,7 @@ exports.useExportDataContext = useExportDataContext;
1078
1202
  exports.useFeatureBranchQueryString = useFeatureBranchQueryString;
1079
1203
  exports.useFeatureFlags = useFeatureFlags;
1080
1204
  exports.useFilterBarContext = useFilterBarContext;
1205
+ exports.useGeolocation = useGeolocation;
1081
1206
  exports.useHasAccessTo = useHasAccessTo;
1082
1207
  exports.useImageUploader = useImageUploader;
1083
1208
  exports.useIrisAppId = useIrisAppId;
package/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
- import { AnalyticsContext, AssetSortingContext, ConfirmationDialogContext, EnvironmentContext, ErrorHandlingContext, ExportDataContext, FeatureFlagContext, FilterBarContext, TokenContext, ModalDialogContext, NavigationContext, OemBrandingContext, UserSubscriptionContext, TimeRangeContext, ToastContext, CurrentUserContext, CurrentUserPreferenceContext, WidgetConfigContext } from '@trackunit/react-core-contexts-api';
2
- import { useContext, useMemo, useState, useCallback, useReducer, useEffect, useRef } from 'react';
1
+ import { AnalyticsContext, AssetSortingContext, ConfirmationDialogContext, EnvironmentContext, ErrorHandlingContext, ExportDataContext, FeatureFlagContext, FilterBarContext, GeolocationContext, TokenContext, ModalDialogContext, NavigationContext, OemBrandingContext, UserSubscriptionContext, TimeRangeContext, ToastContext, CurrentUserContext, CurrentUserPreferenceContext, WidgetConfigContext } from '@trackunit/react-core-contexts-api';
2
+ import { useContext, useMemo, useState, useCallback, useEffect, useReducer, useRef } from 'react';
3
3
  import { isEqual } from 'es-toolkit';
4
4
  import { AssetRuntime, CustomerRuntime, EventRuntime, ParamsRuntime, SiteRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
5
5
 
@@ -228,6 +228,99 @@ const useFilterBarContext = () => {
228
228
  return context;
229
229
  };
230
230
 
231
+ const useStandaloneGeolocation = ({ enabled, requestOnMount, }) => {
232
+ const [position, setPosition] = useState(null);
233
+ const queryGeolocationPermission = useCallback(async () => {
234
+ if (!enabled || typeof navigator === "undefined") {
235
+ return null;
236
+ }
237
+ try {
238
+ const status = await navigator.permissions.query({ name: "geolocation" });
239
+ return status.state;
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }, [enabled]);
245
+ const getCurrentPosition = useCallback(async () => {
246
+ if (!enabled || typeof navigator === "undefined") {
247
+ return null;
248
+ }
249
+ return await new Promise(resolve => {
250
+ try {
251
+ navigator.geolocation.getCurrentPosition(currentPosition => {
252
+ resolve([currentPosition.coords.longitude, currentPosition.coords.latitude]);
253
+ }, () => {
254
+ resolve(null);
255
+ });
256
+ }
257
+ catch {
258
+ resolve(null);
259
+ }
260
+ });
261
+ }, [enabled]);
262
+ const getPosition = useCallback(async (options) => {
263
+ const prompt = options?.prompt !== false;
264
+ if (!prompt) {
265
+ const permissionState = await queryGeolocationPermission();
266
+ if (permissionState !== "granted") {
267
+ return null;
268
+ }
269
+ }
270
+ const currentPosition = await getCurrentPosition();
271
+ if (currentPosition !== null) {
272
+ setPosition(currentPosition);
273
+ }
274
+ return currentPosition;
275
+ }, [getCurrentPosition, queryGeolocationPermission]);
276
+ useEffect(() => {
277
+ if (!enabled || requestOnMount !== true) {
278
+ return;
279
+ }
280
+ void queryGeolocationPermission()
281
+ .then(permissionState => {
282
+ if (permissionState !== "granted") {
283
+ return null;
284
+ }
285
+ return getCurrentPosition();
286
+ })
287
+ .then(currentPosition => {
288
+ if (currentPosition !== null) {
289
+ setPosition(currentPosition);
290
+ }
291
+ });
292
+ }, [enabled, getCurrentPosition, queryGeolocationPermission, requestOnMount]);
293
+ return useMemo(() => ({
294
+ position,
295
+ getPosition,
296
+ }), [getPosition, position]);
297
+ };
298
+ /**
299
+ * Hook providing geolocation capabilities.
300
+ *
301
+ * In the host, geolocation is resolved directly via the browser API.
302
+ * In Iris Apps, requests are proxied to the host via the iframe bridge,
303
+ * so permission is only requested once at the host level.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * const { position, getPosition } = useGeolocation();
308
+ *
309
+ * // User-initiated request (e.g. "My Location" button)
310
+ * onClick: () => {
311
+ * void getPosition().then(pos => { if (pos) fitBounds(pos); });
312
+ * }
313
+ * ```
314
+ */
315
+ const useGeolocation = (options) => {
316
+ const context = useContext(GeolocationContext);
317
+ const standaloneGeolocation = useStandaloneGeolocation({
318
+ enabled: context === null,
319
+ requestOnMount: options?.requestOnMount,
320
+ });
321
+ return context ?? standaloneGeolocation;
322
+ };
323
+
231
324
  /**
232
325
  * This is a hook to use the TokenContext.
233
326
  *
@@ -394,7 +487,7 @@ const resolveAccess = async (context, options) => {
394
487
  }
395
488
  return context.hasAccessTo(options);
396
489
  };
397
- const INITIAL_STATE = {
490
+ const INITIAL_STATE$1 = {
398
491
  status: "loading",
399
492
  hasAccess: undefined,
400
493
  error: undefined,
@@ -402,7 +495,7 @@ const INITIAL_STATE = {
402
495
  const accessReducer = (_state, action) => {
403
496
  switch (action.type) {
404
497
  case "FETCH_START":
405
- return INITIAL_STATE;
498
+ return INITIAL_STATE$1;
406
499
  case "FETCH_SUCCESS":
407
500
  return { status: "success", hasAccess: action.hasAccess, error: undefined };
408
501
  case "FETCH_ERROR":
@@ -425,7 +518,7 @@ const accessReducer = (_state, action) => {
425
518
  const useHasAccessTo = (options) => {
426
519
  const context = useContext(NavigationContext);
427
520
  const [stableOptions, setStableOptions] = useState(options);
428
- const [state, dispatch] = useReducer(accessReducer, INITIAL_STATE);
521
+ const [state, dispatch] = useReducer(accessReducer, INITIAL_STATE$1);
429
522
  if (!isEqual(stableOptions, options)) {
430
523
  setStableOptions(options);
431
524
  }
@@ -574,6 +667,17 @@ const useCustomerRuntime = () => {
574
667
  return { customerInfo, loading, error };
575
668
  };
576
669
 
670
+ const eventRuntimeReducer = (_state, action) => {
671
+ switch (action.type) {
672
+ case "success":
673
+ return { status: "success", eventInfo: action.eventInfo };
674
+ case "error":
675
+ return { status: "error", error: action.error };
676
+ default:
677
+ return _state;
678
+ }
679
+ };
680
+ const INITIAL_STATE = { status: "loading" };
577
681
  /**
578
682
  * A hook to expose event runtime for React components
579
683
  *
@@ -592,25 +696,17 @@ const useCustomerRuntime = () => {
592
696
  * }, [getEventQuery, eventInfo]);
593
697
  */
594
698
  const useEventRuntime = () => {
595
- const [eventInfo, setEventInfo] = useState();
596
- const [loading, setLoading] = useState(true);
597
- const [error, setError] = useState();
699
+ const [state, dispatch] = useReducer(eventRuntimeReducer, INITIAL_STATE);
598
700
  useEffect(() => {
599
- const getEventInfo = async () => {
600
- setLoading(true);
601
- try {
602
- const updatedEventInfo = await EventRuntime.getEventInfo();
603
- setLoading(false);
604
- setEventInfo(updatedEventInfo);
605
- }
606
- catch (e) {
607
- setLoading(false);
608
- setError(new Error("Failed to get event info"));
609
- }
610
- };
611
- void getEventInfo();
701
+ void EventRuntime.getEventInfo()
702
+ .then(eventInfo => dispatch({ type: "success", eventInfo }))
703
+ .catch(() => dispatch({ type: "error", error: new Error("Failed to get event info") }));
612
704
  }, []);
613
- return { eventInfo, loading, error };
705
+ return {
706
+ eventInfo: state.status === "success" ? state.eventInfo : undefined,
707
+ loading: state.status === "loading",
708
+ error: state.status === "error" ? state.error : undefined,
709
+ };
614
710
  };
615
711
 
616
712
  /**
@@ -979,6 +1075,28 @@ const useWidgetConfigAsync = () => {
979
1075
  }
980
1076
  return context;
981
1077
  };
1078
+ const widgetDataReducer = (state, action) => {
1079
+ switch (action.type) {
1080
+ case "dataLoaded":
1081
+ return { ...state, data: action.data, loadingData: false };
1082
+ case "dataVersionLoaded":
1083
+ return { ...state, dataVersion: action.dataVersion };
1084
+ case "titleLoaded":
1085
+ return { ...state, title: action.title };
1086
+ case "dataUpdated":
1087
+ return { ...state, data: action.data, dataVersion: action.dataVersion };
1088
+ case "titleUpdated":
1089
+ return { ...state, title: action.title };
1090
+ default:
1091
+ return state;
1092
+ }
1093
+ };
1094
+ const INITIAL_WIDGET_DATA_STATE = {
1095
+ data: null,
1096
+ loadingData: true,
1097
+ dataVersion: null,
1098
+ title: null,
1099
+ };
982
1100
  /**
983
1101
  * This is a hook to use the WidgetConfigContext.
984
1102
  *
@@ -994,13 +1112,9 @@ const useWidgetConfigAsync = () => {
994
1112
  */
995
1113
  const useWidgetConfig = () => {
996
1114
  const widgetConfigContext = useWidgetConfigAsync();
997
- const [data, setData] = useState(null);
998
- const [loadingData, setLoadingData] = useState(true);
999
- const [dataVersion, setDataVersion] = useState(null);
1000
- const [title, setTitle] = useState(null);
1115
+ const [widgetData, dispatch] = useReducer(widgetDataReducer, INITIAL_WIDGET_DATA_STATE);
1001
1116
  const filters = useFilterBarContext();
1002
1117
  const { timeRange } = useTimeRange();
1003
- // use window.location.hash directly to avoid depending on tanstack router in core-hooks
1004
1118
  const [edit, setEdit] = useState(() => window.location.hash.includes("edit=true"));
1005
1119
  useEffect(() => {
1006
1120
  const handleHashChange = () => {
@@ -1011,29 +1125,27 @@ const useWidgetConfig = () => {
1011
1125
  }, []);
1012
1126
  const widgetConfigContextRef = useRef(widgetConfigContext);
1013
1127
  useEffect(() => {
1014
- void widgetConfigContextRef.current.getData().then(d => {
1015
- setData(d);
1016
- setLoadingData(false);
1017
- });
1018
- void widgetConfigContextRef.current.getDataVersion().then(dv => setDataVersion(dv));
1019
- void widgetConfigContextRef.current.getTitle().then(t => setTitle(t));
1128
+ void widgetConfigContextRef.current.getData().then(d => dispatch({ type: "dataLoaded", data: d }));
1129
+ void widgetConfigContextRef.current
1130
+ .getDataVersion()
1131
+ .then(dv => dispatch({ type: "dataVersionLoaded", dataVersion: dv }));
1132
+ void widgetConfigContextRef.current.getTitle().then(t => dispatch({ type: "titleLoaded", title: t }));
1020
1133
  }, []);
1021
1134
  const result = useMemo(() => ({
1022
- data,
1135
+ data: widgetData.data,
1023
1136
  setData: async (newData, newDataVersion) => {
1024
1137
  await widgetConfigContext.setData(newData, newDataVersion);
1025
- setData(newData);
1026
- setDataVersion(newDataVersion);
1138
+ dispatch({ type: "dataUpdated", data: newData, dataVersion: newDataVersion });
1027
1139
  },
1028
- dataVersion,
1029
- loadingData,
1140
+ dataVersion: widgetData.dataVersion,
1141
+ loadingData: widgetData.loadingData,
1030
1142
  setLoadingState: async (newLoadingState) => {
1031
1143
  await widgetConfigContext.setLoadingState(newLoadingState);
1032
1144
  },
1033
- title,
1145
+ title: widgetData.title,
1034
1146
  setTitle: async (newTitle) => {
1035
1147
  await widgetConfigContext.setTitle(newTitle);
1036
- setTitle(newTitle);
1148
+ dispatch({ type: "titleUpdated", title: newTitle });
1037
1149
  },
1038
1150
  filters,
1039
1151
  timeRange,
@@ -1046,14 +1158,26 @@ const useWidgetConfig = () => {
1046
1158
  if (props) {
1047
1159
  if (props.newData && props.newData.data) {
1048
1160
  await WidgetConfigRuntime.setWidgetData(props.newData.data, props.newData.dataVersion ?? 1, props.newTitle ?? undefined);
1049
- setData(props.newData.data);
1050
- setDataVersion(props.newData.dataVersion ?? 1);
1161
+ dispatch({
1162
+ type: "dataUpdated",
1163
+ data: props.newData.data,
1164
+ dataVersion: props.newData.dataVersion ?? 1,
1165
+ });
1051
1166
  }
1052
1167
  }
1053
1168
  await widgetConfigContext.closeEditMode();
1054
1169
  },
1055
- }), [data, dataVersion, title, filters, timeRange, edit, loadingData, widgetConfigContext]);
1170
+ }), [
1171
+ widgetData.data,
1172
+ widgetData.dataVersion,
1173
+ widgetData.title,
1174
+ filters,
1175
+ timeRange,
1176
+ edit,
1177
+ widgetData.loadingData,
1178
+ widgetConfigContext,
1179
+ ]);
1056
1180
  return result;
1057
1181
  };
1058
1182
 
1059
- export { fetchAssetBlobUrl, useAnalytics, useAssetRuntime, useAssetSorting, useConfirmationDialog, useCurrentUser, useCurrentUserFavoriteAdvancedSensors, useCurrentUserFavoriteInsights, useCurrentUserLanguage, useCurrentUserSystemOfMeasurement, useCurrentUserTimeZonePreference, useCustomerRuntime, useEnvironment, useErrorHandler, useErrorHandlerOrNull, useEventRuntime, useExportDataContext, useFeatureBranchQueryString, useFeatureFlags, useFilterBarContext, useHasAccessTo, useImageUploader, useIrisAppId, useIrisAppImage, useIrisAppName, useModalDialogContext, useNavigateInHost, useOemBrandingContext, useSiteRuntime, useTimeRange, useToast, useToken, useUserPermission, useUserSubscription, useWidgetConfig, useWidgetConfigAsync };
1183
+ export { fetchAssetBlobUrl, useAnalytics, useAssetRuntime, useAssetSorting, useConfirmationDialog, useCurrentUser, useCurrentUserFavoriteAdvancedSensors, useCurrentUserFavoriteInsights, useCurrentUserLanguage, useCurrentUserSystemOfMeasurement, useCurrentUserTimeZonePreference, useCustomerRuntime, useEnvironment, useErrorHandler, useErrorHandlerOrNull, useEventRuntime, useExportDataContext, useFeatureBranchQueryString, useFeatureFlags, useFilterBarContext, useGeolocation, useHasAccessTo, useImageUploader, useIrisAppId, useIrisAppImage, useIrisAppName, useModalDialogContext, useNavigateInHost, useOemBrandingContext, useSiteRuntime, useTimeRange, useToast, useToken, useUserPermission, useUserSubscription, useWidgetConfig, useWidgetConfigAsync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-core-hooks",
3
- "version": "1.12.67",
3
+ "version": "1.13.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -9,9 +9,9 @@
9
9
  "dependencies": {
10
10
  "react": "19.0.0",
11
11
  "es-toolkit": "^1.39.10",
12
- "@trackunit/iris-app-runtime-core": "1.13.60",
13
- "@trackunit/iris-app-runtime-core-api": "1.12.57",
14
- "@trackunit/react-core-contexts-api": "1.13.58"
12
+ "@trackunit/iris-app-runtime-core": "1.14.0",
13
+ "@trackunit/iris-app-runtime-core-api": "1.13.0",
14
+ "@trackunit/react-core-contexts-api": "1.14.0"
15
15
  },
16
16
  "module": "./index.esm.js",
17
17
  "main": "./index.cjs.js",
@@ -0,0 +1,23 @@
1
+ import { type GeolocationContextValue } from "@trackunit/react-core-contexts-api";
2
+ type UseGeolocationOptions = Readonly<{
3
+ requestOnMount?: boolean;
4
+ }>;
5
+ /**
6
+ * Hook providing geolocation capabilities.
7
+ *
8
+ * In the host, geolocation is resolved directly via the browser API.
9
+ * In Iris Apps, requests are proxied to the host via the iframe bridge,
10
+ * so permission is only requested once at the host level.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const { position, getPosition } = useGeolocation();
15
+ *
16
+ * // User-initiated request (e.g. "My Location" button)
17
+ * onClick: () => {
18
+ * void getPosition().then(pos => { if (pos) fitBounds(pos); });
19
+ * }
20
+ * ```
21
+ */
22
+ export declare const useGeolocation: (options?: UseGeolocationOptions) => GeolocationContextValue;
23
+ export {};
package/src/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./exportData/useExportDataContext";
7
7
  export * from "./featureFlags/useFeatureFlags";
8
8
  export * from "./fetchAssetBlobUrl";
9
9
  export * from "./filterBar/useFilterBarContext";
10
+ export * from "./geolocation/useGeolocation";
10
11
  export * from "./images/useImageUploader";
11
12
  export * from "./images/useIrisAppImage";
12
13
  export * from "./modalDialog/useModalDialogContext";