@trackunit/react-core-contexts 2.6.17-alpha-62031fc7c43.0 → 2.7.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
@@ -40,10 +40,25 @@ const FeatureFlagProviderIrisApp = ({ children }) => {
40
40
  return jsxRuntime.jsx(reactCoreContextsApi.FeatureFlagContextProvider, { value: featureFlags, children: children });
41
41
  };
42
42
 
43
+ /**
44
+ * Picks out the messages that report a missing authorization scope.
45
+ *
46
+ * The server offers no structured signal to key off: `verifyScopes`
47
+ * (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
48
+ * `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
49
+ * " or " + " scope". Matching on that shape is therefore the only option available, and it
50
+ * lives here so the HTTP and subscription links cannot drift apart on what counts as a
51
+ * missing scope.
52
+ *
53
+ * Filtering rather than inspecting the first error matters in both links: a response can
54
+ * carry several errors in any order, since the directive throws per resolved field.
55
+ */
56
+ const getMissingScopeMessages = (errors) => errors.map(error => error.message).filter(message => message.startsWith("Missing ") && message.endsWith(" scope"));
57
+
43
58
  /**
44
59
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
45
60
  */
46
- const createErrorLink = ({ errorHandler, getToken, }) => {
61
+ const createErrorLink = ({ errorHandler, getToken, showErrorToast, }) => {
47
62
  return error.onError(({ graphQLErrors, networkError, operation, response, forward }) => {
48
63
  if (networkError) {
49
64
  // We skip the error logging if the error is an AbortError
@@ -70,6 +85,10 @@ const createErrorLink = ({ errorHandler, getToken, }) => {
70
85
  }
71
86
  // eslint-disable-next-line no-console
72
87
  console.error(`Error calling: '${operation.getContext().clientAwareness.name}' fetching Data for: ${operation.operationName}`, graphQLErrors);
88
+ const missingScopeMessages = getMissingScopeMessages(graphQLErrors);
89
+ if (missingScopeMessages.length > 0) {
90
+ showErrorToast(new Error(missingScopeMessages.join(", ")));
91
+ }
73
92
  /**
74
93
  * We want to see the full graphQL error since
75
94
  * it contains extra details like the query/mutation
@@ -120,18 +139,20 @@ const DEFAULT_AUTH_GRACE_MS = 30000;
120
139
  /**
121
140
  * Wraps an SSE subscription link and provides the same error monitoring as the
122
141
  * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
123
- * and UNAUTHENTICATED codes for long-lived subscription Observables.
142
+ * and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
143
+ * long-lived subscription Observables.
124
144
  *
125
145
  * UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
126
146
  * Okta's autoRenew to refresh the token. If a successful response arrives
127
147
  * within the window the capture is cancelled; if the timer fires, a single
128
148
  * captureException is sent — an actionable signal that token refresh failed.
129
149
  */
130
- const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
150
+ const createSubscriptionErrorLink = ({ errorHandler, getToken, showErrorToast, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
131
151
  return new client.ApolloLink((operation, forward) => {
132
152
  return new client.Observable(observer => {
133
153
  let authGraceTimer = null;
134
154
  let authErrorReported = false;
155
+ let missingScopeReported = false;
135
156
  const clearGraceTimer = () => {
136
157
  if (authGraceTimer !== null) {
137
158
  clearTimeout(authGraceTimer);
@@ -161,6 +182,16 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
161
182
  level: "error",
162
183
  data: { log: JSON.stringify(errors) },
163
184
  });
185
+ // A subscription is long-lived and a scope failure is static for the operation, so
186
+ // the same error can arrive on every payload. Report it once per subscription
187
+ // rather than re-raising the toast, which the provider's de-dup would otherwise
188
+ // make visibly disappear and re-enter. Reset when a clean payload arrives, as the
189
+ // auth grace handling below does.
190
+ const missingScopeMessages = getMissingScopeMessages(errors);
191
+ if (missingScopeMessages.length > 0 && !missingScopeReported) {
192
+ missingScopeReported = true;
193
+ showErrorToast(new Error(missingScopeMessages.join(", ")));
194
+ }
164
195
  const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
165
196
  x.message.includes("Invalid token specified") ||
166
197
  x.message.includes("Access denied! You need to be authorized to perform this action!"));
@@ -180,6 +211,7 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
180
211
  else {
181
212
  clearGraceTimer();
182
213
  authErrorReported = false;
214
+ missingScopeReported = false;
183
215
  }
184
216
  observer.next(response);
185
217
  },
@@ -281,7 +313,7 @@ const isInternalGqlContext = () => {
281
313
  /**
282
314
  * @internal
283
315
  */
284
- const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
316
+ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }) => {
285
317
  let token = firstToken;
286
318
  let tracingHeaders = initialTracingHeaders;
287
319
  const publicGraphQLLink = client.createHttpLink({
@@ -301,7 +333,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
301
333
  },
302
334
  };
303
335
  });
304
- const errorLink = createErrorLink({ errorHandler, getToken: () => token });
336
+ const errorLink = createErrorLink({ errorHandler, getToken: () => token, showErrorToast });
305
337
  const defaultOptions = {
306
338
  watchQuery: {
307
339
  fetchPolicy: "no-cache",
@@ -339,7 +371,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
339
371
  headers: () => generateHeaders(token, tracingHeaders),
340
372
  });
341
373
  // Split links based on operation type
342
- const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
374
+ const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token, showErrorToast });
343
375
  const splitLink = client.from([
344
376
  authLink,
345
377
  client.split(({ query }) => {
@@ -394,6 +426,34 @@ const useApolloClient = () => {
394
426
  const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = reactCoreHooks.useEnvironment();
395
427
  const { token: currentToken } = reactCoreHooks.useToken();
396
428
  const errorHandler = reactCoreHooks.useErrorHandler();
429
+ const { addToast } = reactCoreHooks.useToast();
430
+ const { permissions } = reactCoreHooks.useCurrentUser();
431
+ const showErrorToast = react.useCallback((error) => {
432
+ // Only a developer running an Iris App locally can act on a missing scope, since only
433
+ // they can edit the manifest. The server throws the same message for real customer
434
+ // sessions and for token clients that have no manifest at all, so surfacing it outside
435
+ // local mode tells users to fix something they cannot reach.
436
+ //
437
+ // Read from storage rather than through useIrisAppsSDK().developerMode: that context is
438
+ // created below this provider (it queries GraphQL, so it needs this Apollo client) and
439
+ // lives in a visibility:host library this one cannot import.
440
+ const isDeveloper = permissions?.some(permission => permission === "account.iris.app.developer");
441
+ if (!isDeveloper) {
442
+ return;
443
+ }
444
+ void addToast({
445
+ // ToastProviderHost de-dups on id *or* title, so both must be constant and
446
+ // specific: a message-derived id churned on every scope combination, and the
447
+ // generic "Error" title evicted unrelated toasts (and was evicted by them).
448
+ id: "apollo-missing-scope",
449
+ intent: "danger",
450
+ title: "Missing scope",
451
+ // A missing scope leaves the view permanently empty, so the only explanation
452
+ // the user gets must not time out.
453
+ behavior: "persistent",
454
+ description: `${error.message} - you must add the scope to your iris app manifest`,
455
+ });
456
+ }, [addToast, permissions]);
397
457
  const [client] = react.useState(() => {
398
458
  return createApolloClient({
399
459
  /* DON'T CHANGE THIS ! its there to ensure we don't recreate apollo client just because the token changes,
@@ -406,6 +466,7 @@ const useApolloClient = () => {
406
466
  tracingHeaders,
407
467
  firstToken: currentToken,
408
468
  errorHandler,
469
+ showErrorToast,
409
470
  });
410
471
  });
411
472
  // Synchronously propagate the token into the Apollo client closure during render
@@ -973,7 +1034,7 @@ const TrackunitProviders = ({ translations, children, errorHandler }) => {
973
1034
  i18nLibraryTranslation.registerTranslations(translations); // Register the apps translations if passed.
974
1035
  }
975
1036
  i18nLibraryTranslation.initializeTranslationsForApp(); // Initialize all registered translations
976
- return (jsxRuntime.jsx(EnvironmentProviderIrisApp, { children: jsxRuntime.jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsxRuntime.jsx(ThemeCssProviderIrisApp, { children: jsxRuntime.jsx(TokenProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserPreferenceProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserProviderIrisApp, { children: jsxRuntime.jsx(UserSubscriptionProviderIrisApp, { children: jsxRuntime.jsx(AnalyticsProviderIrisApp, { children: jsxRuntime.jsx(OemBrandingProviderIrisApp, { children: jsxRuntime.jsx(AssetSortingProviderIrisApp, { children: jsxRuntime.jsx(ManagerApolloProvider, { children: jsxRuntime.jsx(NavigationProviderIrisApp, { children: jsxRuntime.jsx(ToastProviderIrisApp, { children: jsxRuntime.jsx(ModalDialogContextProviderIrisApp, { children: jsxRuntime.jsx(ConfirmationDialogProviderIrisApp, { children: jsxRuntime.jsx(FilterBarProviderIrisApp, { children: jsxRuntime.jsx(ExportDataProviderIrisApp, { children: jsxRuntime.jsx(TimeRangeProviderIrisApp, { children: jsxRuntime.jsx(WidgetConfigProviderIrisApp, { children: jsxRuntime.jsx(GeolocationProviderIrisApp, { children: jsxRuntime.jsx(react.Suspense, { fallback: jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsxRuntime.jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
1037
+ return (jsxRuntime.jsx(EnvironmentProviderIrisApp, { children: jsxRuntime.jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsxRuntime.jsx(ThemeCssProviderIrisApp, { children: jsxRuntime.jsx(TokenProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserPreferenceProviderIrisApp, { children: jsxRuntime.jsx(CurrentUserProviderIrisApp, { children: jsxRuntime.jsx(UserSubscriptionProviderIrisApp, { children: jsxRuntime.jsx(AnalyticsProviderIrisApp, { children: jsxRuntime.jsx(OemBrandingProviderIrisApp, { children: jsxRuntime.jsx(AssetSortingProviderIrisApp, { children: jsxRuntime.jsx(ToastProviderIrisApp, { children: jsxRuntime.jsx(ManagerApolloProvider, { children: jsxRuntime.jsx(NavigationProviderIrisApp, { children: jsxRuntime.jsx(ModalDialogContextProviderIrisApp, { children: jsxRuntime.jsx(ConfirmationDialogProviderIrisApp, { children: jsxRuntime.jsx(FilterBarProviderIrisApp, { children: jsxRuntime.jsx(ExportDataProviderIrisApp, { children: jsxRuntime.jsx(TimeRangeProviderIrisApp, { children: jsxRuntime.jsx(WidgetConfigProviderIrisApp, { children: jsxRuntime.jsx(GeolocationProviderIrisApp, { children: jsxRuntime.jsx(react.Suspense, { fallback: jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsxRuntime.jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
977
1038
  };
978
1039
 
979
1040
  exports.FeatureFlagProviderIrisApp = FeatureFlagProviderIrisApp;
package/index.esm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { FeatureFlagRuntime, ToastRuntime, AnalyticsRuntime, registerHostChangeHandler, AssetSortingRuntime, ConfirmationDialogRuntime, EnvironmentRuntime, ExportDataRuntime, AssetsFilterBarRuntime, CustomersFilterBarRuntime, SitesFilterBarRuntime, GeolocationRuntime, ModalDialogRuntime, NavigationRuntime, OemBrandingRuntime, ThemeCssRuntime, TimeRangeRuntime, TokenRuntime, CurrentUserRuntime, CurrentUserPreferenceRuntime, UserSubscriptionRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
3
3
  import { FeatureFlagContextProvider, ToastProvider, AnalyticsContextProvider, AssetSortingProvider, ConfirmationDialogProvider, EnvironmentContextProvider, ErrorHandlingContextProvider, ExportDataContext, FilterBarProvider, GeolocationProvider, ModalDialogContextProvider, NavigationContextProvider, OemBrandingContextProvider, TimeRangeProvider, TokenProvider, CurrentUserProvider, CurrentUserPreferenceProvider, UserSubscriptionProvider, WidgetConfigProvider } from '@trackunit/react-core-contexts-api';
4
- import { useState, useEffect, useMemo, useCallback, useReducer, Suspense } from 'react';
4
+ import { useState, useEffect, useCallback, useMemo, useReducer, Suspense } from 'react';
5
5
  import { ApolloLink, Observable, createHttpLink, from, split, ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
6
- import { useEnvironment, useToken, useErrorHandler } from '@trackunit/react-core-hooks';
6
+ import { useEnvironment, useToken, useErrorHandler, useToast, useCurrentUser } from '@trackunit/react-core-hooks';
7
7
  import { setContext } from '@apollo/client/link/context';
8
8
  import { removeTypenameFromVariables } from '@apollo/client/link/remove-typename';
9
9
  import { getMainDefinition, Observable as Observable$1 } from '@apollo/client/utilities';
@@ -38,10 +38,25 @@ const FeatureFlagProviderIrisApp = ({ children }) => {
38
38
  return jsx(FeatureFlagContextProvider, { value: featureFlags, children: children });
39
39
  };
40
40
 
41
+ /**
42
+ * Picks out the messages that report a missing authorization scope.
43
+ *
44
+ * The server offers no structured signal to key off: `verifyScopes`
45
+ * (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
46
+ * `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
47
+ * " or " + " scope". Matching on that shape is therefore the only option available, and it
48
+ * lives here so the HTTP and subscription links cannot drift apart on what counts as a
49
+ * missing scope.
50
+ *
51
+ * Filtering rather than inspecting the first error matters in both links: a response can
52
+ * carry several errors in any order, since the directive throws per resolved field.
53
+ */
54
+ const getMissingScopeMessages = (errors) => errors.map(error => error.message).filter(message => message.startsWith("Missing ") && message.endsWith(" scope"));
55
+
41
56
  /**
42
57
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
43
58
  */
44
- const createErrorLink = ({ errorHandler, getToken, }) => {
59
+ const createErrorLink = ({ errorHandler, getToken, showErrorToast, }) => {
45
60
  return onError(({ graphQLErrors, networkError, operation, response, forward }) => {
46
61
  if (networkError) {
47
62
  // We skip the error logging if the error is an AbortError
@@ -68,6 +83,10 @@ const createErrorLink = ({ errorHandler, getToken, }) => {
68
83
  }
69
84
  // eslint-disable-next-line no-console
70
85
  console.error(`Error calling: '${operation.getContext().clientAwareness.name}' fetching Data for: ${operation.operationName}`, graphQLErrors);
86
+ const missingScopeMessages = getMissingScopeMessages(graphQLErrors);
87
+ if (missingScopeMessages.length > 0) {
88
+ showErrorToast(new Error(missingScopeMessages.join(", ")));
89
+ }
71
90
  /**
72
91
  * We want to see the full graphQL error since
73
92
  * it contains extra details like the query/mutation
@@ -118,18 +137,20 @@ const DEFAULT_AUTH_GRACE_MS = 30000;
118
137
  /**
119
138
  * Wraps an SSE subscription link and provides the same error monitoring as the
120
139
  * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
121
- * and UNAUTHENTICATED codes for long-lived subscription Observables.
140
+ * and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
141
+ * long-lived subscription Observables.
122
142
  *
123
143
  * UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
124
144
  * Okta's autoRenew to refresh the token. If a successful response arrives
125
145
  * within the window the capture is cancelled; if the timer fires, a single
126
146
  * captureException is sent — an actionable signal that token refresh failed.
127
147
  */
128
- const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
148
+ const createSubscriptionErrorLink = ({ errorHandler, getToken, showErrorToast, graceMs = DEFAULT_AUTH_GRACE_MS, }) => {
129
149
  return new ApolloLink((operation, forward) => {
130
150
  return new Observable(observer => {
131
151
  let authGraceTimer = null;
132
152
  let authErrorReported = false;
153
+ let missingScopeReported = false;
133
154
  const clearGraceTimer = () => {
134
155
  if (authGraceTimer !== null) {
135
156
  clearTimeout(authGraceTimer);
@@ -159,6 +180,16 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
159
180
  level: "error",
160
181
  data: { log: JSON.stringify(errors) },
161
182
  });
183
+ // A subscription is long-lived and a scope failure is static for the operation, so
184
+ // the same error can arrive on every payload. Report it once per subscription
185
+ // rather than re-raising the toast, which the provider's de-dup would otherwise
186
+ // make visibly disappear and re-enter. Reset when a clean payload arrives, as the
187
+ // auth grace handling below does.
188
+ const missingScopeMessages = getMissingScopeMessages(errors);
189
+ if (missingScopeMessages.length > 0 && !missingScopeReported) {
190
+ missingScopeReported = true;
191
+ showErrorToast(new Error(missingScopeMessages.join(", ")));
192
+ }
162
193
  const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
163
194
  x.message.includes("Invalid token specified") ||
164
195
  x.message.includes("Access denied! You need to be authorized to perform this action!"));
@@ -178,6 +209,7 @@ const createSubscriptionErrorLink = ({ errorHandler, getToken, graceMs = DEFAULT
178
209
  else {
179
210
  clearGraceTimer();
180
211
  authErrorReported = false;
212
+ missingScopeReported = false;
181
213
  }
182
214
  observer.next(response);
183
215
  },
@@ -279,7 +311,7 @@ const isInternalGqlContext = () => {
279
311
  /**
280
312
  * @internal
281
313
  */
282
- const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
314
+ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }) => {
283
315
  let token = firstToken;
284
316
  let tracingHeaders = initialTracingHeaders;
285
317
  const publicGraphQLLink = createHttpLink({
@@ -299,7 +331,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
299
331
  },
300
332
  };
301
333
  });
302
- const errorLink = createErrorLink({ errorHandler, getToken: () => token });
334
+ const errorLink = createErrorLink({ errorHandler, getToken: () => token, showErrorToast });
303
335
  const defaultOptions = {
304
336
  watchQuery: {
305
337
  fetchPolicy: "no-cache",
@@ -337,7 +369,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
337
369
  headers: () => generateHeaders(token, tracingHeaders),
338
370
  });
339
371
  // Split links based on operation type
340
- const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
372
+ const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token, showErrorToast });
341
373
  const splitLink = from([
342
374
  authLink,
343
375
  split(({ query }) => {
@@ -392,6 +424,34 @@ const useApolloClient = () => {
392
424
  const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = useEnvironment();
393
425
  const { token: currentToken } = useToken();
394
426
  const errorHandler = useErrorHandler();
427
+ const { addToast } = useToast();
428
+ const { permissions } = useCurrentUser();
429
+ const showErrorToast = useCallback((error) => {
430
+ // Only a developer running an Iris App locally can act on a missing scope, since only
431
+ // they can edit the manifest. The server throws the same message for real customer
432
+ // sessions and for token clients that have no manifest at all, so surfacing it outside
433
+ // local mode tells users to fix something they cannot reach.
434
+ //
435
+ // Read from storage rather than through useIrisAppsSDK().developerMode: that context is
436
+ // created below this provider (it queries GraphQL, so it needs this Apollo client) and
437
+ // lives in a visibility:host library this one cannot import.
438
+ const isDeveloper = permissions?.some(permission => permission === "account.iris.app.developer");
439
+ if (!isDeveloper) {
440
+ return;
441
+ }
442
+ void addToast({
443
+ // ToastProviderHost de-dups on id *or* title, so both must be constant and
444
+ // specific: a message-derived id churned on every scope combination, and the
445
+ // generic "Error" title evicted unrelated toasts (and was evicted by them).
446
+ id: "apollo-missing-scope",
447
+ intent: "danger",
448
+ title: "Missing scope",
449
+ // A missing scope leaves the view permanently empty, so the only explanation
450
+ // the user gets must not time out.
451
+ behavior: "persistent",
452
+ description: `${error.message} - you must add the scope to your iris app manifest`,
453
+ });
454
+ }, [addToast, permissions]);
395
455
  const [client] = useState(() => {
396
456
  return createApolloClient({
397
457
  /* DON'T CHANGE THIS ! its there to ensure we don't recreate apollo client just because the token changes,
@@ -404,6 +464,7 @@ const useApolloClient = () => {
404
464
  tracingHeaders,
405
465
  firstToken: currentToken,
406
466
  errorHandler,
467
+ showErrorToast,
407
468
  });
408
469
  });
409
470
  // Synchronously propagate the token into the Apollo client closure during render
@@ -971,7 +1032,7 @@ const TrackunitProviders = ({ translations, children, errorHandler }) => {
971
1032
  registerTranslations(translations); // Register the apps translations if passed.
972
1033
  }
973
1034
  initializeTranslationsForApp(); // Initialize all registered translations
974
- return (jsx(EnvironmentProviderIrisApp, { children: jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsx(ThemeCssProviderIrisApp, { children: jsx(TokenProviderIrisApp, { children: jsx(CurrentUserPreferenceProviderIrisApp, { children: jsx(CurrentUserProviderIrisApp, { children: jsx(UserSubscriptionProviderIrisApp, { children: jsx(AnalyticsProviderIrisApp, { children: jsx(OemBrandingProviderIrisApp, { children: jsx(AssetSortingProviderIrisApp, { children: jsx(ManagerApolloProvider, { children: jsx(NavigationProviderIrisApp, { children: jsx(ToastProviderIrisApp, { children: jsx(ModalDialogContextProviderIrisApp, { children: jsx(ConfirmationDialogProviderIrisApp, { children: jsx(FilterBarProviderIrisApp, { children: jsx(ExportDataProviderIrisApp, { children: jsx(TimeRangeProviderIrisApp, { children: jsx(WidgetConfigProviderIrisApp, { children: jsx(GeolocationProviderIrisApp, { children: jsx(Suspense, { fallback: jsx(Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
1035
+ return (jsx(EnvironmentProviderIrisApp, { children: jsx(ErrorHandlingProviderIrisApp, { errorHandler: errorHandler, children: jsx(ThemeCssProviderIrisApp, { children: jsx(TokenProviderIrisApp, { children: jsx(CurrentUserPreferenceProviderIrisApp, { children: jsx(CurrentUserProviderIrisApp, { children: jsx(UserSubscriptionProviderIrisApp, { children: jsx(AnalyticsProviderIrisApp, { children: jsx(OemBrandingProviderIrisApp, { children: jsx(AssetSortingProviderIrisApp, { children: jsx(ToastProviderIrisApp, { children: jsx(ManagerApolloProvider, { children: jsx(NavigationProviderIrisApp, { children: jsx(ModalDialogContextProviderIrisApp, { children: jsx(ConfirmationDialogProviderIrisApp, { children: jsx(FilterBarProviderIrisApp, { children: jsx(ExportDataProviderIrisApp, { children: jsx(TimeRangeProviderIrisApp, { children: jsx(WidgetConfigProviderIrisApp, { children: jsx(GeolocationProviderIrisApp, { children: jsx(Suspense, { fallback: jsx(Spinner, { centering: "centered", "data-testid": "trackunit-providers" }), children: jsx(FeatureFlagProviderIrisApp, { children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
975
1036
  };
976
1037
 
977
1038
  export { FeatureFlagProviderIrisApp, ManagerApolloProvider, ToastProviderIrisApp, TrackunitProviders };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/core-contexts/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@trackunit/react-core-contexts",
3
- "version": "2.6.17-alpha-62031fc7c43.0",
3
+ "version": "2.7.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/iris-app-api": "2.4.12-alpha-62031fc7c43.0",
11
- "@trackunit/iris-app-runtime-core-api": "1.17.24-alpha-62031fc7c43.0",
12
- "@trackunit/react-core-hooks": "1.21.18-alpha-62031fc7c43.0",
13
- "@trackunit/i18n-library-translation": "2.4.18-alpha-62031fc7c43.0",
14
- "@trackunit/react-components": "2.10.12-alpha-62031fc7c43.0",
15
- "@trackunit/iris-app-runtime-core": "1.18.24-alpha-62031fc7c43.0",
10
+ "@trackunit/iris-app-api": "2.4.12",
11
+ "@trackunit/iris-app-runtime-core-api": "1.17.24",
12
+ "@trackunit/react-core-hooks": "1.21.18",
13
+ "@trackunit/i18n-library-translation": "2.4.18",
14
+ "@trackunit/react-components": "2.10.12",
15
+ "@trackunit/iris-app-runtime-core": "1.18.24",
16
16
  "graphql-sse": "^2.5.4",
17
- "@trackunit/react-core-contexts-api": "1.18.24-alpha-62031fc7c43.0"
17
+ "@trackunit/react-core-contexts-api": "1.18.24"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@apollo/client": "^3.13.8",
@@ -3,7 +3,7 @@ import { ErrorHandlingContextValue, TracingHeaders } from "@trackunit/iris-app-r
3
3
  /**
4
4
  * @internal
5
5
  */
6
- export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }: {
6
+ export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, showErrorToast, }: {
7
7
  graphqlPublicUrl: string;
8
8
  graphqlInternalUrl: string;
9
9
  graphqlReportUrl: string;
@@ -11,6 +11,7 @@ export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl
11
11
  isDev: boolean;
12
12
  firstToken?: string;
13
13
  errorHandler: ErrorHandlingContextValue;
14
+ showErrorToast: (error: Error) => void;
14
15
  }) => {
15
16
  client: ApolloClient<import("@apollo/client").NormalizedCacheObject>;
16
17
  setToken: (newToken: string | undefined) => void;
@@ -3,7 +3,8 @@ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api"
3
3
  /**
4
4
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
5
5
  */
6
- export declare const createErrorLink: ({ errorHandler, getToken, }: {
6
+ export declare const createErrorLink: ({ errorHandler, getToken, showErrorToast, }: {
7
7
  errorHandler: ErrorHandlingContextValue;
8
8
  getToken: () => string | undefined;
9
+ showErrorToast: (error: Error) => void;
9
10
  }) => ApolloLink;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Picks out the messages that report a missing authorization scope.
3
+ *
4
+ * The server offers no structured signal to key off: `verifyScopes`
5
+ * (libs/graphql/common/src/schemabuilder/AuthorizationDirective.ts) throws a plain
6
+ * `ForbiddenError` whose message is assembled as "Missing " + the allowed scopes joined with
7
+ * " or " + " scope". Matching on that shape is therefore the only option available, and it
8
+ * lives here so the HTTP and subscription links cannot drift apart on what counts as a
9
+ * missing scope.
10
+ *
11
+ * Filtering rather than inspecting the first error matters in both links: a response can
12
+ * carry several errors in any order, since the directive throws per resolved field.
13
+ */
14
+ export declare const getMissingScopeMessages: (errors: ReadonlyArray<{
15
+ message: string;
16
+ }>) => Array<string>;
@@ -3,15 +3,17 @@ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api"
3
3
  /**
4
4
  * Wraps an SSE subscription link and provides the same error monitoring as the
5
5
  * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
6
- * and UNAUTHENTICATED codes for long-lived subscription Observables.
6
+ * and UNAUTHENTICATED codes, and surfacing missing authorization scopes — for
7
+ * long-lived subscription Observables.
7
8
  *
8
9
  * UNAUTHENTICATED errors are deferred by a grace period (default 30s) to allow
9
10
  * Okta's autoRenew to refresh the token. If a successful response arrives
10
11
  * within the window the capture is cancelled; if the timer fires, a single
11
12
  * captureException is sent — an actionable signal that token refresh failed.
12
13
  */
13
- export declare const createSubscriptionErrorLink: ({ errorHandler, getToken, graceMs, }: {
14
+ export declare const createSubscriptionErrorLink: ({ errorHandler, getToken, showErrorToast, graceMs, }: {
14
15
  errorHandler: ErrorHandlingContextValue;
15
16
  getToken: () => string | undefined;
17
+ showErrorToast: (error: Error) => void;
16
18
  graceMs?: number;
17
19
  }) => ApolloLink;