@salesforce-ui-embedding/internal-lightning-ui-embedding 2.3.0-rc.2 → 2.3.0-rc.3

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.
@@ -3,6 +3,9 @@
3
3
  * For full license text, see the LICENSE.txt file in this package.
4
4
  */
5
5
 
6
+ import { NavigationMixin } from "lightning/navigation";
7
+ import LightningToast from "lightning/toast";
8
+ import UiEmbeddingModal from "lightning/uiEmbeddingModal";
6
9
  import { api, LightningElement } from "lwc";
7
10
 
8
11
  import { buildBootstrapEnvelope, validateHeartbeat, validateShutdown } from "./utils/bootstrap";
@@ -11,6 +14,7 @@ import { isLightningDomainSameOriginEnabled } from "./utils/gater";
11
14
  import { HostBridge } from "./utils/host-bridge";
12
15
  import { HOST_INFO } from "./utils/host-info";
13
16
  import { createInstrumentation } from "./utils/instrumentation";
17
+ import { StandardErrorCode } from "./utils/jsonrpc";
14
18
  import { createLogger } from "./utils/log";
15
19
  import { computeSandboxTokens } from "./utils/sandbox";
16
20
  import {
@@ -19,6 +23,7 @@ import {
19
23
  EVENTS_UNSUBSCRIBE_METHOD,
20
24
  HEARTBEAT_TYPE,
21
25
  HostLwcEvent,
26
+ JsonRpcHandlerError,
22
27
  RESIZE_METHOD,
23
28
  SHUTDOWN_TYPE,
24
29
  UI_STATE_CHANGED_METHOD,
@@ -51,7 +56,18 @@ function toCloneableDetail(detail) {
51
56
  return detail;
52
57
  }
53
58
 
54
- export default class Embedding extends LightningElement {
59
+ /** `ui/display-modal` component spec of the built-in generic modal, the only spec the host opens. */
60
+ const DEFAULT_MODAL_SPEC = "default";
61
+
62
+ /**
63
+ * Modal config keys the host owns; dropped from guest `attributes` so a guest cannot set them. Documented on
64
+ * `ModalOptions.attributes` in the contract — keep the two in sync.
65
+ */
66
+ const RESERVED_MODAL_KEYS = new Set(["size", "label", "description", "disableClose"]);
67
+
68
+ // `NavigationMixin(LightningElement)` gives the host `this[NavigationMixin.Navigate](pageRef)`
69
+ // so `ui/navigate-to` can drive real Lightning navigation.
70
+ export default class Embedding extends NavigationMixin(LightningElement) {
55
71
  _iframe = null;
56
72
  _container = null;
57
73
  _frameShadow = null;
@@ -614,7 +630,8 @@ export default class Embedding extends LightningElement {
614
630
  // subscribe/unsubscribe registry, and the resize-hint notification; it
615
631
  // calls back into _composeUiState() for the snapshot returned on
616
632
  // subscribe, _redispatchEventFromEmbedding() for inbound
617
- // `ui/events/dispatch`, and _handleResizeHint() for `ui/notifications/resize`.
633
+ // `ui/events/dispatch`, _handleResizeHint() for `ui/notifications/resize`, and
634
+ // _handleNavigateTo() / _handleDisplayModal() / _handleDisplayToast() for the view.* requests.
618
635
  this._bridge = new HostBridge(
619
636
  port,
620
637
  () => this._composeUiState(),
@@ -623,6 +640,77 @@ export default class Embedding extends LightningElement {
623
640
  (params) => this._handleSfEmbeddingError(params),
624
641
  (...args) => this._log(...args),
625
642
  this._instrumentation,
643
+ (params) => this._handleNavigateTo(params),
644
+ (params) => this._handleDisplayModal(params),
645
+ (params) => this._handleDisplayToast(params),
646
+ );
647
+ }
648
+
649
+ /** `ui/navigate-to` → lightning/navigation. Params are PageReference-shaped. */
650
+ _handleNavigateTo(opts) {
651
+ const navigate = this[NavigationMixin.Navigate];
652
+
653
+ navigate.call(this, {
654
+ type: opts.type,
655
+ attributes: opts.attributes ?? {},
656
+ ...(opts.state ? { state: opts.state } : {}),
657
+ });
658
+ }
659
+
660
+ /**
661
+ * `ui/display-modal` → lightning/modal. Resolves on user dismissal (indefinite class). Only the generic
662
+ * "default" modal is offered; any other spec is rejected with INVALID_PARAMS (§7.1).
663
+ */
664
+ async _handleDisplayModal(opts) {
665
+ if (opts.component !== DEFAULT_MODAL_SPEC) {
666
+ throw new JsonRpcHandlerError(
667
+ StandardErrorCode.INVALID_PARAMS,
668
+ `unregistered modal component: '${opts.component}'`,
669
+ );
670
+ }
671
+ // Guest attributes first so they cannot override the host-controlled modal config. Only
672
+ // defined values are passed: an explicit `undefined` would overwrite the modal's defaults
673
+ // (e.g. size 'medium').
674
+ const attributes = Object.fromEntries(
675
+ Object.entries(opts.attributes ?? {}).filter(([key]) => !RESERVED_MODAL_KEYS.has(key)),
676
+ );
677
+ const config = {
678
+ ...attributes,
679
+ ...(opts.size !== undefined ? { size: opts.size } : {}),
680
+ ...(opts.label !== undefined ? { label: opts.label, description: opts.label } : {}),
681
+ };
682
+ let closeResult;
683
+ try {
684
+ closeResult = await UiEmbeddingModal.open(config);
685
+ } catch (err) {
686
+ // The overlay container rejects with a bare `undefined` when it won't open another
687
+ // overlay (per-type limit reached); surface a real error instead of an empty rejection.
688
+ if (err instanceof Error) throw err;
689
+ // Keep any other rejection value as the cause so the original failure stays diagnosable.
690
+ throw new JsonRpcHandlerError(StandardErrorCode.INTERNAL_ERROR, "host could not open the modal", {
691
+ cause: err,
692
+ });
693
+ }
694
+ // A modal may close with an LWC proxy, which fails structured-clone; the transport
695
+ // swallows that send error, so the guest's indefinite request would never settle.
696
+ return { closeResult: toCloneableDetail(closeResult) };
697
+ }
698
+
699
+ /**
700
+ * `ui/display-toast` → lightning/toast. Fire-and-forget: response means queued/displayed, NOT dismissed
701
+ * (§7.1). `source` is the host element so the toast anchors to this instance.
702
+ */
703
+ _handleDisplayToast(opts) {
704
+ // `lightning/toast` requires `label`, and (unlike the legacy ShowToastEvent path) does not
705
+ // promote `message` to it, so a title-less toast would fail validation. Promote it here.
706
+ LightningToast.show(
707
+ {
708
+ label: opts.title ?? opts.message,
709
+ message: opts.title ? opts.message : undefined,
710
+ variant: opts.variant,
711
+ mode: opts.mode,
712
+ },
713
+ this,
626
714
  );
627
715
  }
628
716
 
@@ -15,6 +15,8 @@ import { MessageChannelTransport, StandardErrorCode } from "./jsonrpc";
15
15
  import { createLogger } from "./log";
16
16
  import { BridgeClient, JsonRpcRouter } from "./request-router";
17
17
  import {
18
+ DISPLAY_MODAL_METHOD,
19
+ DISPLAY_TOAST_METHOD,
18
20
  EMBEDDING_INITIALIZED_METHOD,
19
21
  ERROR_METHOD,
20
22
  EVENTS_DISPATCH_METHOD,
@@ -22,10 +24,12 @@ import {
22
24
  EVENTS_UNSUBSCRIBE_METHOD,
23
25
  HOST_INITIALIZED_METHOD,
24
26
  JsonRpcHandlerError,
27
+ NAVIGATE_TO_METHOD,
25
28
  RESIZE_METHOD,
26
29
  UI_STATE_SUBSCRIBE_METHOD,
27
30
  UI_STATE_UNSUBSCRIBE_METHOD,
28
31
  } from "./sf-embedding-contract";
32
+ import { isHttpUrl } from "./url";
29
33
 
30
34
  export class HostBridge extends JsonRpcRouter {
31
35
  // Active UiState wire subscription. Only one is permitted at a time; a second
@@ -48,6 +52,9 @@ export class HostBridge extends JsonRpcRouter {
48
52
  onSfEmbeddingError,
49
53
  log = createLogger("[lightning-ui-embedding:host-bridge]", () => false),
50
54
  instrumentation,
55
+ onNavigateTo,
56
+ onDisplayModal,
57
+ onDisplayToast,
51
58
  ) {
52
59
  // Wrap the transferred port in the shared transport; the BridgeClient
53
60
  // subscribes to it (lazily starting the port), and the router claims
@@ -131,6 +138,77 @@ export class HostBridge extends JsonRpcRouter {
131
138
  log("embedding error received:", params);
132
139
  onSfEmbeddingError(params);
133
140
  });
141
+
142
+ // view.* UI-action methods. Each is registered only when the component supplies its
143
+ // callback; when absent the router returns METHOD_NOT_FOUND (the host simply doesn't
144
+ // offer that surface). Capability gating (§6) will layer on top of this — a later story
145
+ // wires CAPABILITY_NOT_ALLOWED.
146
+
147
+ // Navigate request; component drives lightning/navigation. Result: void.
148
+ if (onNavigateTo) {
149
+ this.onRequest(NAVIGATE_TO_METHOD, async (params) => {
150
+ if (!params || typeof params.type !== "string" || params.type.length === 0) {
151
+ this._instrumentation?.incrementCounter("sfembedding.view.navigate_rejected", 1, true, {
152
+ reason: "invalid_params",
153
+ });
154
+ throw new JsonRpcHandlerError(
155
+ StandardErrorCode.INVALID_PARAMS,
156
+ "navigate-to requires a 'type' string",
157
+ );
158
+ }
159
+ if (params.type === "standard__webPage" && !isHttpUrl(params.attributes?.url)) {
160
+ this._instrumentation?.incrementCounter("sfembedding.view.navigate_rejected", 1, true, {
161
+ reason: "invalid_url",
162
+ });
163
+ throw new JsonRpcHandlerError(
164
+ StandardErrorCode.INVALID_PARAMS,
165
+ "navigate-to standard__webPage requires an http(s) or relative 'url'",
166
+ );
167
+ }
168
+ await onNavigateTo(params);
169
+ this._instrumentation?.incrementCounter("sfembedding.view.navigated", 1, false);
170
+ });
171
+ }
172
+
173
+ // Modal request; component opens lightning/modal. Indefinite class: resolves on user
174
+ // dismissal. Unregistered component specs are rejected with INVALID_PARAMS (§7.1) — the
175
+ // component throws for them.
176
+ if (onDisplayModal) {
177
+ this.onRequest(DISPLAY_MODAL_METHOD, async (params) => {
178
+ if (!params || typeof params.component !== "string" || params.component.length === 0) {
179
+ this._instrumentation?.incrementCounter("sfembedding.view.modal_rejected", 1, true, {
180
+ reason: "invalid_params",
181
+ });
182
+ throw new JsonRpcHandlerError(
183
+ StandardErrorCode.INVALID_PARAMS,
184
+ "display-modal requires a 'component' spec",
185
+ );
186
+ }
187
+ this._instrumentation?.incrementCounter("sfembedding.view.modal_opened", 1, false);
188
+ const result = await onDisplayModal(params);
189
+ this._instrumentation?.incrementCounter("sfembedding.view.modal_closed", 1, false);
190
+ return result;
191
+ });
192
+ }
193
+
194
+ // Toast request; component shows lightning/toast. Result: void. The response means
195
+ // "queued/displayed", NOT "dismissed" (§7.1) — mirrors native lightning/toast's
196
+ // fire-and-forget contract.
197
+ if (onDisplayToast) {
198
+ this.onRequest(DISPLAY_TOAST_METHOD, async (params) => {
199
+ if (!params || typeof params.message !== "string" || params.message.length === 0) {
200
+ this._instrumentation?.incrementCounter("sfembedding.view.toast_rejected", 1, true, {
201
+ reason: "invalid_params",
202
+ });
203
+ throw new JsonRpcHandlerError(
204
+ StandardErrorCode.INVALID_PARAMS,
205
+ "display-toast requires a 'message'",
206
+ );
207
+ }
208
+ await onDisplayToast(params);
209
+ this._instrumentation?.incrementCounter("sfembedding.view.toast_shown", 1, false);
210
+ });
211
+ }
134
212
  }
135
213
 
136
214
  /** The active UiState wire subscription id, or null when none is active. */
@@ -3,11 +3,11 @@
3
3
  * For full license text, see the LICENSE.txt file in this package.
4
4
  */
5
5
 
6
- // `"2.3.0-rc.2"` is a repo-wide ambient declared in /types/globals.d.ts;
6
+ // `"2.3.0-rc.3"` is a repo-wide ambient declared in /types/globals.d.ts;
7
7
  // build.mjs substitutes the literal at build time, jest globals supply a test value.
8
8
 
9
9
  /** Identity emitted on `ui/notifications/host-initialized`. */
10
10
  export const HOST_INFO = {
11
11
  name: "lightning-ui-embedding",
12
- version: "2.3.0-rc.2",
12
+ version: "2.3.0-rc.3",
13
13
  };
@@ -40,8 +40,8 @@ const HOST_META_DATA_PARAM = "hostMetaData";
40
40
  /** Throw from a request handler to surface a specific JSON-RPC error code. */
41
41
  class JsonRpcHandlerError extends Error {
42
42
 
43
- constructor(code, message) {
44
- super(message);_defineProperty(this, "code", void 0);
43
+ constructor(code, message, options) {
44
+ super(message, options);_defineProperty(this, "code", void 0);
45
45
  this.code = code;
46
46
  this.name = "JsonRpcHandlerError";
47
47
  }
@@ -55,12 +55,24 @@ const EMBEDDING_INITIALIZED_METHOD = "ui/notifications/embedding-initialized";
55
55
  /** Iframe-side error notification. Fire-and-forget, non-PII observability signal. */
56
56
  const ERROR_METHOD = "ui/notifications/error";
57
57
 
58
+ // Host modal: embedding → host request that backs the guest's `lightning/modal` import (§7.1 / §7.2).
59
+ /** `ui/display-modal` — backs `lightning/modal`. Capability gate: `view.displayModal`. */
60
+ const DISPLAY_MODAL_METHOD = "ui/display-modal";
61
+
62
+ // Host navigation: embedding → host request that backs the guest's `lightning/navigation` import (§7.1 / §7.2).
63
+ /** `ui/navigate-to` — backs `lightning/navigation`. Capability gate: `view.navigateTo`. */
64
+ const NAVIGATE_TO_METHOD = "ui/navigate-to";
65
+
58
66
  // Resize hint channel: embedding → host, fire-and-forget. CSS pixels.
59
67
  const RESIZE_METHOD = "ui/notifications/resize";
60
68
 
69
+ // Host toast: embedding → host request that backs the guest's `lightning/toast` import (§7.1 / §7.2).
70
+ /** `ui/display-toast` — backs `lightning/toast`. Capability gate: `view.displayToast`. */
71
+ const DISPLAY_TOAST_METHOD = "ui/display-toast";
72
+
61
73
  // UI-state subscription axis. Snapshot in subscribe response; full-replacement change pushes.
62
74
  const UI_STATE_SUBSCRIBE_METHOD = "ui/subscribe/ui-state";
63
75
  const UI_STATE_UNSUBSCRIBE_METHOD = "ui/unsubscribe/ui-state";
64
76
  const UI_STATE_CHANGED_METHOD = "ui/notifications/ui-state-changed";
65
77
 
66
- export { BOOTSTRAP_ENVELOPE_TYPE, EMBEDDING_INITIALIZED_METHOD, ERROR_METHOD, EVENTS_DISPATCH_METHOD, EVENTS_SUBSCRIBE_METHOD, EVENTS_UNSUBSCRIBE_METHOD, HEARTBEAT_TYPE, HOST_INITIALIZED_METHOD, HOST_META_DATA_PARAM, HostLwcEvent, JsonRpcHandlerError, PROTOCOL_NAME, RESIZE_METHOD, SHUTDOWN_TYPE, UI_STATE_CHANGED_METHOD, UI_STATE_SUBSCRIBE_METHOD, UI_STATE_UNSUBSCRIBE_METHOD };
78
+ export { BOOTSTRAP_ENVELOPE_TYPE, DISPLAY_MODAL_METHOD, DISPLAY_TOAST_METHOD, EMBEDDING_INITIALIZED_METHOD, ERROR_METHOD, EVENTS_DISPATCH_METHOD, EVENTS_SUBSCRIBE_METHOD, EVENTS_UNSUBSCRIBE_METHOD, HEARTBEAT_TYPE, HOST_INITIALIZED_METHOD, HOST_META_DATA_PARAM, HostLwcEvent, JsonRpcHandlerError, NAVIGATE_TO_METHOD, PROTOCOL_NAME, RESIZE_METHOD, SHUTDOWN_TYPE, UI_STATE_CHANGED_METHOD, UI_STATE_SUBSCRIBE_METHOD, UI_STATE_UNSUBSCRIBE_METHOD };
@@ -28,6 +28,20 @@ export function isLightningDomain(origin) {
28
28
  );
29
29
  }
30
30
 
31
+ /**
32
+ * True when `url` is a string that resolves (against the host page) to an http(s) URL. Rejects
33
+ * `javascript:`, `data:` and other schemes a guest could otherwise hand to host navigation.
34
+ */
35
+ export function isHttpUrl(url) {
36
+ if (typeof url !== "string" || url.length === 0) return false;
37
+ try {
38
+ const { protocol } = new URL(url, window.location.href);
39
+ return protocol === "https:" || protocol === "http:";
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
31
45
  export function appendHostMetaData(src, instanceId, hostAppOrigin) {
32
46
  const url = new URL(src);
33
47
  const meta = { instanceId, hostAppOrigin };
@@ -0,0 +1,14 @@
1
+ <!--
2
+ Copyright 2026 Salesforce, Inc. All rights reserved.
3
+ For full license text, see the LICENSE.txt file in this package.
4
+ -->
5
+
6
+ <template>
7
+ <lightning-modal-header label={headerLabel}></lightning-modal-header>
8
+ <lightning-modal-body>
9
+ <p class="slds-p-around_small">{content}</p>
10
+ </lightning-modal-body>
11
+ <lightning-modal-footer>
12
+ <lightning-button label="Close" variant="brand" onclick={handleClose}></lightning-button>
13
+ </lightning-modal-footer>
14
+ </template>
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright 2026 Salesforce, Inc. All rights reserved.
3
+ * For full license text, see the LICENSE.txt file in this package.
4
+ */
5
+
6
+ import LightningModal from "lightning/modal";
7
+ import { api } from "lwc";
8
+
9
+ const DEFAULT_HEADING = "Message";
10
+
11
+ /**
12
+ * Generic modal `<lightning-ui-embedding>` registers by default, so a guest's `displayModal` works without the
13
+ * host registering a component. It shows guest-supplied text only (rendered as text, never as markup) and
14
+ * closes without a result; richer modals are registered by the host.
15
+ */
16
+ export default class UiEmbeddingModal extends LightningModal {
17
+ @api heading = "";
18
+ @api content = "";
19
+
20
+ /** Header text; never empty, so the dialog always has an accessible name. */
21
+ get headerLabel() {
22
+ return this.heading || this.label || DEFAULT_HEADING;
23
+ }
24
+
25
+ handleClose() {
26
+ this.close();
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Copyright 2026 Salesforce, Inc. All rights reserved.
4
+ For full license text, see the LICENSE.txt file in this package.
5
+ -->
6
+ <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
7
+ <isExposed>false</isExposed>
8
+ <minApiVersion>62.0</minApiVersion>
9
+ </LightningComponentBundle>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce-ui-embedding/internal-lightning-ui-embedding",
3
- "version": "2.3.0-rc.2",
3
+ "version": "2.3.0-rc.3",
4
4
  "private": false,
5
5
  "description": "Lightning Base Component (LWC) host for sf-embedding-protocol iframes",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@salesforce/jsonrpc": "^10.12.3",
25
- "@salesforce/sf-embedding-contract": "2.3.0-rc.2",
26
- "utils": "2.3.0-rc.2"
25
+ "@salesforce/sf-embedding-contract": "2.3.0-rc.3",
26
+ "utils": "2.3.0-rc.3"
27
27
  },
28
28
  "sideEffects": false,
29
29
  "publishConfig": {