@netsapiens/horizon-sdk 0.1.2 → 0.1.4

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/dist/index.d.ts CHANGED
@@ -141,12 +141,86 @@ interface ThemeTokens {
141
141
  * components instead of bringing their own MUI/styling stack — that keeps the
142
142
  * remote bundle small and ensures visual consistency with Horizon.
143
143
  */
144
+ /** One step of a {@link HorizonUITemplates.FormPanel} multi-step wizard. */
145
+ interface FormPanelStep {
146
+ /** Label shown in the stepper header. */
147
+ label: string;
148
+ /** Content rendered when this step is active. */
149
+ content: React.ReactNode;
150
+ /**
151
+ * Optional gate run before advancing past this step (Next / forward header
152
+ * click). Return false to block. Async supported (e.g. react-hook-form `trigger`).
153
+ */
154
+ validate?: () => boolean | Promise<boolean>;
155
+ }
144
156
  interface HorizonUITemplates {
145
157
  PageTemplate: React.ComponentType<unknown>;
146
158
  PageTemplateWithExtensions: React.ComponentType<unknown>;
147
159
  FormTemplate: React.ComponentType<unknown>;
148
- SideTrayTemplate: React.ComponentType<unknown>;
149
- DatagridTemplate: React.ComponentType<unknown>;
160
+ /**
161
+ * Shared right-side drawer shell: sticky header (icon/title/subtitle/close),
162
+ * scrollable body, optional sticky footer. Use for detail/view/settings panels.
163
+ */
164
+ SidePanel: React.ComponentType<{
165
+ open: boolean;
166
+ onClose: () => void;
167
+ title: React.ReactNode;
168
+ subtitle?: React.ReactNode;
169
+ icon?: React.ReactNode;
170
+ width?: "sm" | "md" | "lg" | "xl";
171
+ footer?: React.ReactNode;
172
+ disableBackdropClick?: boolean;
173
+ children: React.ReactNode;
174
+ }>;
175
+ /**
176
+ * SidePanel + form semantics: a <form>, a standard Submit/Cancel footer, and
177
+ * the `form-section-before` / `form-section-after` extension zones built in.
178
+ * Pass `steps` to render a multi-step wizard (stepper header + Back/Next/Submit
179
+ * footer) instead. Use this for add/edit forms.
180
+ */
181
+ FormPanel: React.ComponentType<{
182
+ open: boolean;
183
+ onClose: () => void;
184
+ title: React.ReactNode;
185
+ subtitle?: React.ReactNode;
186
+ icon?: React.ReactNode;
187
+ width?: "sm" | "md" | "lg" | "xl";
188
+ formType: string;
189
+ mode: "add" | "edit";
190
+ entityId?: string | number;
191
+ formData?: Record<string, unknown>;
192
+ onSubmit?: (event?: React.BaseSyntheticEvent) => void | Promise<void>;
193
+ submitLabel?: React.ReactNode;
194
+ cancelLabel?: React.ReactNode;
195
+ isSubmitting?: boolean;
196
+ submitDisabled?: boolean;
197
+ error?: Error | string | null;
198
+ /** Multi-step wizard: pass one step per page of fields. */
199
+ steps?: FormPanelStep[];
200
+ onReset?: () => void;
201
+ isComplete?: boolean;
202
+ loading?: boolean;
203
+ children?: React.ReactNode;
204
+ }>;
205
+ DatagridTemplate: React.ComponentType<{
206
+ data: unknown[];
207
+ columns: unknown[];
208
+ actions?: unknown[];
209
+ toolbar?: {
210
+ enableSearch?: boolean;
211
+ searchPlaceholder?: string;
212
+ enableExport?: boolean;
213
+ enableFilter?: boolean;
214
+ enableColumns?: boolean;
215
+ };
216
+ defaultPageSize?: number;
217
+ /**
218
+ * Node rendered inline within the toolbar row, alongside search and date
219
+ * range controls. Use for status filter chips (ToggleButtonGroup) and
220
+ * `table-filter-bar` extension zone renderers.
221
+ */
222
+ filterBar?: React.ReactNode;
223
+ }>;
150
224
  Icon: React.ComponentType<{
151
225
  name: string;
152
226
  size?: number | string;
@@ -207,11 +281,11 @@ interface HorizonUI {
207
281
  IconButton?: React.ComponentType<{
208
282
  icon: string;
209
283
  iconSize?: number | string;
210
- 'aria-label'?: string;
211
- color?: 'default' | 'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning';
284
+ "aria-label"?: string;
285
+ color?: "default" | "inherit" | "primary" | "secondary" | "error" | "info" | "success" | "warning";
212
286
  disabled?: boolean;
213
- edge?: 'start' | 'end' | false;
214
- size?: 'small' | 'medium' | 'large';
287
+ edge?: "start" | "end" | false;
288
+ size?: "small" | "medium" | "large";
215
289
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
216
290
  sx?: Record<string, unknown>;
217
291
  }>;
@@ -250,7 +324,7 @@ interface HorizonContext {
250
324
  user: HorizonUser;
251
325
  auth: HorizonAuth;
252
326
  api: HorizonApiClient;
253
- theme: 'light' | 'dark';
327
+ theme: "light" | "dark";
254
328
  locale: string;
255
329
  /**
256
330
  * Host's i18next translation function. All host strings (common, telecom,
@@ -311,8 +385,54 @@ interface RemoteModuleConfig {
311
385
  * Generic extension zones available throughout Horizon. Pages may also define
312
386
  * custom string zone IDs — `ExtensionZone` accepts both.
313
387
  */
314
- type ExtensionZoneId = 'page-header-actions' | 'page-header-secondary' | 'page-content-before' | 'page-content-after' | 'page-sidebar' | 'table-row-actions' | 'table-toolbar' | 'detail-panel-tabs' | 'detail-panel-actions' | 'inbound-call-content' | 'topbar-actions' | 'form-section-before' | 'form-section-after';
388
+ type ExtensionZoneId = "page-header-actions" | "page-header-secondary" | "page-content-after" | "sidetray" | "table-row-actions" | "table-toolbar" | "table-filter-bar" | "inbound-call-content" | "topbar-actions" | "form-section-before" | "form-section-after";
315
389
  type ExtensionZone = ExtensionZoneId | (string & {});
390
+ /**
391
+ * Context passed via `pageContext` to extension components registered in the
392
+ * `'table-filter-bar'` zone. Extensions provide a filter function that the host
393
+ * applies to table rows. This keeps filtering logic in the extension where the
394
+ * business rules live, and keeps the host generic.
395
+ *
396
+ * @example
397
+ * export function RecordingFilter({ context }: ExtensionComponentProps) {
398
+ * const filterCtx = context.pageContext as TableFilterBarContext | undefined;
399
+ * const { ToggleButtonGroup } = context.ui ?? {};
400
+ * const [active, setActive] = useState(false);
401
+ *
402
+ * function handleChange(_e: React.SyntheticEvent, value: string | null) {
403
+ * const next = value === 'recorded';
404
+ * setActive(next);
405
+ * if (next) {
406
+ * filterCtx?.onFilterChange((row) => {
407
+ * const r = row as Record<string, unknown>;
408
+ * return r['call-record-keep'] === 'yes' || r['call-record-keep'] === true;
409
+ * });
410
+ * } else {
411
+ * filterCtx?.onFilterChange(null);
412
+ * }
413
+ * }
414
+ *
415
+ * if (!ToggleButtonGroup) return null;
416
+ * return (
417
+ * <ToggleButtonGroup
418
+ * value={active ? 'recorded' : null}
419
+ * exclusive
420
+ * onChange={handleChange}
421
+ * options={[{ value: 'recorded', label: '● Recording' }]}
422
+ * />
423
+ * );
424
+ * }
425
+ *
426
+ * // NOTE: Track active state locally with useState — TableFilterBarContext does
427
+ * // not reflect the current filter value back to the extension.
428
+ */
429
+ interface TableFilterBarContext<TRow = any> {
430
+ /**
431
+ * Provide a filter function to apply to table rows. Pass `null` to reset.
432
+ * The function receives a row and returns true to include it, false to hide it.
433
+ */
434
+ onFilterChange: (filterFn: ((row: TRow) => boolean) | null) => void;
435
+ }
316
436
  /**
317
437
  * Route pattern with wildcard (`*`) and named-parameter (`:param`) support.
318
438
  * Example patterns: `/manage/call-logs`, `/manage/*\/call-logs`, `/manage/:domain/users`.
@@ -337,7 +457,7 @@ interface ExtensionContext {
337
457
  ui?: HorizonUI;
338
458
  eventBus?: HorizonEventBus;
339
459
  /** Current color scheme — reactive to host theme changes. */
340
- theme: 'light' | 'dark';
460
+ theme: "light" | "dark";
341
461
  /** Host's i18next translation function — all host strings available immediately. */
342
462
  t?: (key: string, options?: Record<string, unknown>) => string;
343
463
  }
@@ -350,6 +470,22 @@ interface ExtensionComponentProps {
350
470
  /** Provided by modal/dialog hosts to allow extensions to dismiss themselves */
351
471
  close?: () => void;
352
472
  }
473
+ /**
474
+ * A SIP call lifecycle event delivered to apps subscribed via
475
+ * `sdk.subscribeToCallEvents`. The host enriches this further, but every event
476
+ * carries at least a `type` and `callId`.
477
+ */
478
+ interface CallEvent {
479
+ type: CallEventType;
480
+ callId: string;
481
+ [key: string]: unknown;
482
+ }
483
+ /**
484
+ * Known call event types. Authored as an open union so the host can introduce
485
+ * new types without a breaking SDK change, while still offering autocomplete
486
+ * for the common ones.
487
+ */
488
+ type CallEventType = "call-started" | "call-answered" | "call-ended" | "call-missed" | (string & {});
353
489
  /**
354
490
  * Payload accepted by `sdk.registerDynamicExtension`. `appId` is filled in by
355
491
  * the SDK from the value passed to `createRemoteAppSDK`.
@@ -386,9 +522,9 @@ interface DynamicColumnDefinition {
386
522
  filterable?: boolean;
387
523
  hideable?: boolean;
388
524
  resizable?: boolean;
389
- type?: 'string' | 'number' | 'date' | 'dateTime' | 'boolean';
390
- align?: 'left' | 'center' | 'right';
391
- headerAlign?: 'left' | 'center' | 'right';
525
+ type?: "string" | "number" | "date" | "dateTime" | "boolean";
526
+ align?: "left" | "center" | "right";
527
+ headerAlign?: "left" | "center" | "right";
392
528
  renderCell?: (params: {
393
529
  row: Record<string, unknown>;
394
530
  value?: unknown;
@@ -424,19 +560,33 @@ declare class RemoteAppSDK {
424
560
  private routes;
425
561
  private dynamicExtensions;
426
562
  private dynamicColumns;
563
+ private callEventsSubscribed;
427
564
  constructor(eventBus: HorizonEventBus, appId: string);
428
- registerRoute(config: Omit<RouteConfig, 'appId'>): Promise<void>;
565
+ registerRoute(config: Omit<RouteConfig, "appId">): Promise<void>;
429
566
  unregisterRoute(routeId: string): void;
430
567
  /**
431
568
  * Convenience: load a component out of a federated module's webpack
432
569
  * container and register it as a route in one step. Useful when the route
433
570
  * component lives in a sibling exposed module rather than the entry App.
434
571
  */
435
- registerRouteFromModule(routeConfig: Omit<RouteConfig, 'appId' | 'component'>, moduleConfig: RemoteModuleConfig): Promise<void>;
436
- registerDynamicExtension(config: Omit<DynamicExtensionConfig, 'appId'>): void;
572
+ registerRouteFromModule(routeConfig: Omit<RouteConfig, "appId" | "component">, moduleConfig: RemoteModuleConfig): Promise<void>;
573
+ registerDynamicExtension(config: Omit<DynamicExtensionConfig, "appId">): void;
437
574
  unregisterDynamicExtension(extensionId: string): void;
438
- registerDynamicColumn(config: Omit<DynamicColumnConfig, 'appId'>): void;
575
+ registerDynamicColumn(config: Omit<DynamicColumnConfig, "appId">): void;
439
576
  unregisterDynamicColumn(columnId: string): void;
577
+ /**
578
+ * Subscribe to live SIP call events. Routes through the host's
579
+ * `CallEventsManager`, which enforces the `call-events:listen` capability and
580
+ * records the subscription against this app — so the platform knows which
581
+ * apps consume call events and can disable the capability platform-wide.
582
+ *
583
+ * Prefer this over listening to `eventBus.on('call-event')` directly: the raw
584
+ * bus is neither gated nor attributed to your app.
585
+ *
586
+ * @returns an unsubscribe function (also torn down by `cleanup()`).
587
+ */
588
+ subscribeToCallEvents(eventTypes: CallEventType[], callback: (event: CallEvent) => void): () => void;
589
+ unsubscribeFromCallEvents(): void;
440
590
  /**
441
591
  * Unregister everything this SDK instance has registered. Call from your
442
592
  * remote app's unmount/cleanup path — `useRemoteApp` does this for you.
@@ -522,8 +672,8 @@ declare function useHorizonContext(): HorizonContext;
522
672
  * Inside a page component wrapped by `HorizonContextProvider`, both params
523
673
  * can be omitted — the provider context is used automatically.
524
674
  */
525
- declare function useTheme(eventBus?: HorizonContext['eventBus'], initialTheme?: 'light' | 'dark'): {
526
- theme: 'light' | 'dark';
675
+ declare function useTheme(eventBus?: HorizonContext["eventBus"], initialTheme?: "light" | "dark"): {
676
+ theme: "light" | "dark";
527
677
  };
528
678
  /**
529
679
  * Returns the host's translation function and current locale.
@@ -579,7 +729,7 @@ declare function useTheme(eventBus?: HorizonContext['eventBus'], initialTheme?:
579
729
  * }
580
730
  */
581
731
  declare function useLocale(): {
582
- t: HorizonContext['t'];
732
+ t: HorizonContext["t"];
583
733
  locale: string;
584
734
  };
585
735
  /**
@@ -618,12 +768,12 @@ declare function useRemoteApp(horizonContext: HorizonContext, appId: string): {
618
768
  /**
619
769
  * Register a route for the lifetime of the calling component.
620
770
  */
621
- declare function useRoute(eventBus: HorizonContext['eventBus'], appId: string, config: Omit<RouteConfig, 'appId'>): RemoteAppSDK;
771
+ declare function useRoute(eventBus: HorizonContext["eventBus"], appId: string, config: Omit<RouteConfig, "appId">): RemoteAppSDK;
622
772
  /**
623
773
  * Register a route by pulling its component out of a federated module's
624
774
  * webpack container.
625
775
  */
626
- declare function useRouteFromModule(eventBus: HorizonContext['eventBus'], appId: string, routeConfig: Omit<RouteConfig, 'appId' | 'component'>, moduleConfig: RemoteModuleConfig): {
776
+ declare function useRouteFromModule(eventBus: HorizonContext["eventBus"], appId: string, routeConfig: Omit<RouteConfig, "appId" | "component">, moduleConfig: RemoteModuleConfig): {
627
777
  loading: boolean;
628
778
  error: Error | null;
629
779
  sdk: RemoteAppSDK;
@@ -632,17 +782,41 @@ declare function useRouteFromModule(eventBus: HorizonContext['eventBus'], appId:
632
782
  * Register a dynamic extension (pattern-based UI injection) for the lifetime
633
783
  * of the calling component.
634
784
  */
635
- declare function useDynamicExtension(eventBus: HorizonContext['eventBus'], appId: string, config: Omit<DynamicExtensionConfig, 'appId'>): RemoteAppSDK;
785
+ declare function useDynamicExtension(eventBus: HorizonContext["eventBus"], appId: string, config: Omit<DynamicExtensionConfig, "appId">): RemoteAppSDK;
786
+ /**
787
+ * Returns the current page context as a typed value.
788
+ *
789
+ * The host passes page-specific data (e.g. active filters, date range, selected
790
+ * row) through `ExtensionContext.pageContext`. This hook casts it to the caller's
791
+ * expected shape and keeps it reactive — when the host updates `pageContext` the
792
+ * extension re-renders automatically because it flows in as a React prop.
793
+ *
794
+ * Define a typed interface for each page's context in your remote app and pass
795
+ * it as the type parameter. Returns `undefined` when the host has not populated
796
+ * any context for the current zone.
797
+ *
798
+ * @example
799
+ * interface CallLogsPageContext {
800
+ * dateRange?: [Date | null, Date | null];
801
+ * }
802
+ *
803
+ * export function AnalyticsWidget({ context }: ExtensionComponentProps) {
804
+ * const pageCtx = usePageContext<CallLogsPageContext>(context);
805
+ * const [start, end] = pageCtx?.dateRange ?? [null, null];
806
+ * // ...
807
+ * }
808
+ */
809
+ declare function usePageContext<T = unknown>(context: ExtensionContext): T | undefined;
636
810
  /**
637
811
  * Register a dynamic table column for the lifetime of the calling component.
638
812
  */
639
- declare function useDynamicColumn(eventBus: HorizonContext['eventBus'], appId: string, config: Omit<DynamicColumnConfig, 'appId'>): RemoteAppSDK;
813
+ declare function useDynamicColumn(eventBus: HorizonContext["eventBus"], appId: string, config: Omit<DynamicColumnConfig, "appId">): RemoteAppSDK;
640
814
 
641
815
  /**
642
816
  * Federation Error
643
817
  * Structured error class with error codes for better error handling
644
818
  */
645
- type HorizonSDKErrorCode = 'PERMISSION_DENIED' | 'RATE_LIMIT_EXCEEDED' | 'INVALID_MESSAGE' | 'SIGNATURE_VERIFICATION_FAILED' | 'API_ERROR' | 'NETWORK_ERROR' | 'INVALID_EXTENSION_POINT' | 'INVALID_CONFIGURATION' | 'APP_NOT_FOUND' | 'MODULE_LOAD_FAILED' | 'INITIALIZATION_FAILED' | 'UNKNOWN_ERROR';
819
+ type HorizonSDKErrorCode = "PERMISSION_DENIED" | "RATE_LIMIT_EXCEEDED" | "INVALID_MESSAGE" | "SIGNATURE_VERIFICATION_FAILED" | "API_ERROR" | "NETWORK_ERROR" | "INVALID_EXTENSION_POINT" | "INVALID_CONFIGURATION" | "APP_NOT_FOUND" | "MODULE_LOAD_FAILED" | "INITIALIZATION_FAILED" | "UNKNOWN_ERROR";
646
820
  interface HorizonSDKErrorOptions {
647
821
  code: HorizonSDKErrorCode;
648
822
  message: string;
@@ -858,4 +1032,4 @@ type AnchorId = (typeof MANAGE_ANCHORS)[keyof typeof MANAGE_ANCHORS] | (typeof P
858
1032
 
859
1033
  declare const VERSION: string;
860
1034
 
861
- export { ANCHORS, APPS_ANCHORS, type AnchorId, type BreadcrumbItem, type DynamicColumnConfig, type DynamicColumnDefinition, type DynamicExtensionConfig, type ExtensionComponentProps, type ExtensionContext, type ExtensionZone, type ExtensionZoneId, type HorizonApiClient, type HorizonAuth, type HorizonContext, HorizonContextProvider, type HorizonEventBus, HorizonSDKError, type HorizonSDKErrorCode, type HorizonSDKErrorOptions, type HorizonUI, type HorizonUITemplates, type HorizonUser, MANAGE_ANCHORS, MY_ACCOUNT_ANCHORS, PLATFORM_ANCHORS, RemoteAppSDK, type RemoteAuthError, type RemoteAuthOptions, type RemoteAuthRequest, type RemoteAuthResponse, type RemoteModuleConfig, type RouteConfig, type RoutePattern, type SemanticPlacement, type ThemeTokens, VERSION, apiError, createHorizonSDKError, createLogger, createRemoteAppSDK, getLogLevel, invalidExtensionPointError, moduleLoadError, permissionDeniedError, rateLimitError, setLogLevel, signatureVerificationError, useDynamicColumn, useDynamicExtension, useHorizonContext, useLocale, useRemoteApp, useRoute, useRouteFromModule, useTheme };
1035
+ export { ANCHORS, APPS_ANCHORS, type AnchorId, type BreadcrumbItem, type CallEvent, type CallEventType, type DynamicColumnConfig, type DynamicColumnDefinition, type DynamicExtensionConfig, type ExtensionComponentProps, type ExtensionContext, type ExtensionZone, type ExtensionZoneId, type FormPanelStep, type HorizonApiClient, type HorizonAuth, type HorizonContext, HorizonContextProvider, type HorizonEventBus, HorizonSDKError, type HorizonSDKErrorCode, type HorizonSDKErrorOptions, type HorizonUI, type HorizonUITemplates, type HorizonUser, MANAGE_ANCHORS, MY_ACCOUNT_ANCHORS, PLATFORM_ANCHORS, RemoteAppSDK, type RemoteAuthError, type RemoteAuthOptions, type RemoteAuthRequest, type RemoteAuthResponse, type RemoteModuleConfig, type RouteConfig, type RoutePattern, type SemanticPlacement, type TableFilterBarContext, type ThemeTokens, VERSION, apiError, createHorizonSDKError, createLogger, createRemoteAppSDK, getLogLevel, invalidExtensionPointError, moduleLoadError, permissionDeniedError, rateLimitError, setLogLevel, signatureVerificationError, useDynamicColumn, useDynamicExtension, useHorizonContext, useLocale, usePageContext, useRemoteApp, useRoute, useRouteFromModule, useTheme };
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@ import { createContext, useState, useEffect, useMemo, createElement, useContext,
3
3
 
4
4
  // src/utils/logger.ts
5
5
  var isDevelopment = process.env.NODE_ENV === "development";
6
- loglevel.setDefaultLevel(isDevelopment ? loglevel.levels.DEBUG : loglevel.levels.WARN);
6
+ loglevel.setDefaultLevel(
7
+ isDevelopment ? loglevel.levels.DEBUG : loglevel.levels.WARN
8
+ );
7
9
  var createLogger = (namespace) => {
8
10
  const moduleLogger = loglevel.getLogger(`HorizonSDK:${namespace}`);
9
11
  if (isDevelopment) {
@@ -41,6 +43,7 @@ var RemoteAppSDK = class {
41
43
  routes = /* @__PURE__ */ new Set();
42
44
  dynamicExtensions = /* @__PURE__ */ new Set();
43
45
  dynamicColumns = /* @__PURE__ */ new Set();
46
+ callEventsSubscribed = false;
44
47
  constructor(eventBus, appId) {
45
48
  this.eventBus = eventBus;
46
49
  this.appId = appId;
@@ -53,7 +56,9 @@ var RemoteAppSDK = class {
53
56
  const route = { ...config, appId: this.appId };
54
57
  this.eventBus.emit("route:register", route);
55
58
  this.routes.add(route.id);
56
- log.info(`[${this.appId}] Route registered: ${route.id} at ${route.parentPath}/${route.path}`);
59
+ log.info(
60
+ `[${this.appId}] Route registered: ${route.id} at ${route.parentPath}/${route.path}`
61
+ );
57
62
  }
58
63
  unregisterRoute(routeId) {
59
64
  this.eventBus.emit("route:unregister", { id: routeId });
@@ -98,7 +103,9 @@ var RemoteAppSDK = class {
98
103
  const column = { ...config, appId: this.appId };
99
104
  this.eventBus.emit("dynamic-column:register", column);
100
105
  this.dynamicColumns.add(column.id);
101
- log.info(`[${this.appId}] Dynamic column registered: ${column.id} \u2192 zone ${column.zone}`);
106
+ log.info(
107
+ `[${this.appId}] Dynamic column registered: ${column.id} \u2192 zone ${column.zone}`
108
+ );
102
109
  }
103
110
  unregisterDynamicColumn(columnId) {
104
111
  this.eventBus.emit("dynamic-column:unregister", { id: columnId });
@@ -106,6 +113,38 @@ var RemoteAppSDK = class {
106
113
  log.info(`[${this.appId}] Dynamic column unregistered: ${columnId}`);
107
114
  }
108
115
  // -------------------------------------------------------------------------
116
+ // Call events (capability-gated, app-scoped)
117
+ // -------------------------------------------------------------------------
118
+ /**
119
+ * Subscribe to live SIP call events. Routes through the host's
120
+ * `CallEventsManager`, which enforces the `call-events:listen` capability and
121
+ * records the subscription against this app — so the platform knows which
122
+ * apps consume call events and can disable the capability platform-wide.
123
+ *
124
+ * Prefer this over listening to `eventBus.on('call-event')` directly: the raw
125
+ * bus is neither gated nor attributed to your app.
126
+ *
127
+ * @returns an unsubscribe function (also torn down by `cleanup()`).
128
+ */
129
+ subscribeToCallEvents(eventTypes, callback) {
130
+ this.eventBus.emit("call-events:subscribe", {
131
+ appId: this.appId,
132
+ eventTypes,
133
+ callback
134
+ });
135
+ this.callEventsSubscribed = true;
136
+ log.info(
137
+ `[${this.appId}] Subscribed to call events: ${eventTypes.join(", ")}`
138
+ );
139
+ return () => this.unsubscribeFromCallEvents();
140
+ }
141
+ unsubscribeFromCallEvents() {
142
+ if (!this.callEventsSubscribed) return;
143
+ this.eventBus.emit("call-events:unsubscribe", { appId: this.appId });
144
+ this.callEventsSubscribed = false;
145
+ log.info(`[${this.appId}] Unsubscribed from call events`);
146
+ }
147
+ // -------------------------------------------------------------------------
109
148
  // Lifecycle
110
149
  // -------------------------------------------------------------------------
111
150
  /**
@@ -120,7 +159,10 @@ var RemoteAppSDK = class {
120
159
  this.dynamicExtensions.forEach(
121
160
  (id) => this.eventBus.emit("dynamic-extension:unregister", { id })
122
161
  );
123
- this.dynamicColumns.forEach((id) => this.eventBus.emit("dynamic-column:unregister", { id }));
162
+ this.dynamicColumns.forEach(
163
+ (id) => this.eventBus.emit("dynamic-column:unregister", { id })
164
+ );
165
+ this.unsubscribeFromCallEvents();
124
166
  this.routes.clear();
125
167
  this.dynamicExtensions.clear();
126
168
  this.dynamicColumns.clear();
@@ -146,13 +188,16 @@ function HorizonContextProvider({
146
188
  context,
147
189
  children
148
190
  }) {
149
- const [theme, setTheme] = useState(context.theme ?? "light");
191
+ const [theme, setTheme] = useState(
192
+ context.theme ?? "light"
193
+ );
150
194
  const [locale, setLocale] = useState(context.locale ?? "en-US");
151
195
  useEffect(() => {
152
196
  if (!context.eventBus) return;
153
197
  const themeHandler = (data) => {
154
198
  const payload = data;
155
- if (payload?.theme === "light" || payload?.theme === "dark") setTheme(payload.theme);
199
+ if (payload?.theme === "light" || payload?.theme === "dark")
200
+ setTheme(payload.theme);
156
201
  };
157
202
  const localeHandler = (data) => {
158
203
  const payload = data;
@@ -171,7 +216,11 @@ function HorizonContextProvider({
171
216
  // theme/locale live. context spread gives access to t, user, api, navigate, etc.
172
217
  [theme, locale]
173
218
  );
174
- return createElement(HorizonContextReact.Provider, { value: liveContext }, children);
219
+ return createElement(
220
+ HorizonContextReact.Provider,
221
+ { value: liveContext },
222
+ children
223
+ );
175
224
  }
176
225
  function useHorizonContext() {
177
226
  const ctx = useContext(HorizonContextReact);
@@ -216,13 +265,13 @@ function useRemoteApp(horizonContext, appId) {
216
265
  sdkRef.current = createRemoteAppSDK(horizonContext.eventBus, appId);
217
266
  }
218
267
  const sdk = sdkRef.current;
219
- useEffect(() => {
220
- return () => sdk.cleanup();
221
- }, [sdk]);
222
268
  return { sdk, ...horizonContext };
223
269
  }
224
270
  function useRoute(eventBus, appId, config) {
225
- const sdk = useMemo(() => createRemoteAppSDK(eventBus, appId), [eventBus, appId]);
271
+ const sdk = useMemo(
272
+ () => createRemoteAppSDK(eventBus, appId),
273
+ [eventBus, appId]
274
+ );
226
275
  useEffect(() => {
227
276
  void sdk.registerRoute(config);
228
277
  return () => sdk.unregisterRoute(config.id);
@@ -230,7 +279,10 @@ function useRoute(eventBus, appId, config) {
230
279
  return sdk;
231
280
  }
232
281
  function useRouteFromModule(eventBus, appId, routeConfig, moduleConfig) {
233
- const sdk = useMemo(() => createRemoteAppSDK(eventBus, appId), [eventBus, appId]);
282
+ const sdk = useMemo(
283
+ () => createRemoteAppSDK(eventBus, appId),
284
+ [eventBus, appId]
285
+ );
234
286
  const [loading, setLoading] = useState(true);
235
287
  const [error, setError] = useState(null);
236
288
  useEffect(() => {
@@ -251,15 +303,24 @@ function useRouteFromModule(eventBus, appId, routeConfig, moduleConfig) {
251
303
  return { loading, error, sdk };
252
304
  }
253
305
  function useDynamicExtension(eventBus, appId, config) {
254
- const sdk = useMemo(() => createRemoteAppSDK(eventBus, appId), [eventBus, appId]);
306
+ const sdk = useMemo(
307
+ () => createRemoteAppSDK(eventBus, appId),
308
+ [eventBus, appId]
309
+ );
255
310
  useEffect(() => {
256
311
  sdk.registerDynamicExtension(config);
257
312
  return () => sdk.unregisterDynamicExtension(config.id);
258
313
  }, [sdk, config]);
259
314
  return sdk;
260
315
  }
316
+ function usePageContext(context) {
317
+ return context.pageContext;
318
+ }
261
319
  function useDynamicColumn(eventBus, appId, config) {
262
- const sdk = useMemo(() => createRemoteAppSDK(eventBus, appId), [eventBus, appId]);
320
+ const sdk = useMemo(
321
+ () => createRemoteAppSDK(eventBus, appId),
322
+ [eventBus, appId]
323
+ );
263
324
  useEffect(() => {
264
325
  sdk.registerDynamicColumn(config);
265
326
  return () => sdk.unregisterDynamicColumn(config.id);
@@ -445,13 +506,13 @@ var ANCHORS = {
445
506
 
446
507
  // package.json
447
508
  var package_default = {
448
- version: "0.1.2"};
509
+ version: "0.1.4"};
449
510
 
450
511
  // src/index.ts
451
512
  var VERSION = package_default.version;
452
513
  var log2 = createLogger("FederationSDK");
453
514
  log2.info(`SDK v${VERSION} loaded`);
454
515
 
455
- export { ANCHORS, APPS_ANCHORS, HorizonContextProvider, HorizonSDKError, MANAGE_ANCHORS, MY_ACCOUNT_ANCHORS, PLATFORM_ANCHORS, RemoteAppSDK, VERSION, apiError, createHorizonSDKError, createLogger, createRemoteAppSDK, getLogLevel, invalidExtensionPointError, moduleLoadError, permissionDeniedError, rateLimitError, setLogLevel, signatureVerificationError, useDynamicColumn, useDynamicExtension, useHorizonContext, useLocale, useRemoteApp, useRoute, useRouteFromModule, useTheme };
516
+ export { ANCHORS, APPS_ANCHORS, HorizonContextProvider, HorizonSDKError, MANAGE_ANCHORS, MY_ACCOUNT_ANCHORS, PLATFORM_ANCHORS, RemoteAppSDK, VERSION, apiError, createHorizonSDKError, createLogger, createRemoteAppSDK, getLogLevel, invalidExtensionPointError, moduleLoadError, permissionDeniedError, rateLimitError, setLogLevel, signatureVerificationError, useDynamicColumn, useDynamicExtension, useHorizonContext, useLocale, usePageContext, useRemoteApp, useRoute, useRouteFromModule, useTheme };
456
517
  //# sourceMappingURL=index.js.map
457
518
  //# sourceMappingURL=index.js.map