Layout Effects

Patient Summary #

There are many summary sections in a patient’s chart, organized by data type. While there is a default ordering, you can use an Effect to reorder them or hide some of them entirely. The PatientChartSummaryConfiguration class helps you craft the effect to do so.

Before and after

The example below shows reordering and hiding or omitting some of the sections:

from canvas_sdk.events import EventType
from canvas_sdk.handlers.base import BaseHandler
from canvas_sdk.effects.patient_chart_summary_configuration import PatientChartSummaryConfiguration


class SummarySectionLayout(BaseHandler):
    RESPONDS_TO = EventType.Name(EventType.PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION)

    def compute(self):
        layout = PatientChartSummaryConfiguration(sections=[
          PatientChartSummaryConfiguration.Section.CARE_TEAMS,
          PatientChartSummaryConfiguration.Section.SOCIAL_DETERMINANTS,
          PatientChartSummaryConfiguration.Section.ALLERGIES,
          PatientChartSummaryConfiguration.Section.CONDITIONS,
          PatientChartSummaryConfiguration.Section.MEDICATIONS,
          PatientChartSummaryConfiguration.Section.VITALS,
        ])

        return [layout.apply()]

Methods #

apply() → Effect #

Sets the order and visibility of the chart summary sections. It is only used in response to the PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION event, and does nothing in any other context.

  • sections is required, with at least one section.

Attributes #

AttributeTypeDescriptionRequired
sectionslist[Section \| CustomSection]The sections to show, in order. Each is a PatientChartSummaryConfiguration.Section or a PatientChartSummaryConfiguration.CustomSection(name=...) for a custom section. Sections you leave out are hidden.Yes

Values in the PatientChartSummaryConfiguration.Section enum are:

ConstantDescription
SOCIAL_DETERMINANTSsocial_determinants
GOALSgoals
CONDITIONSconditions
MEDICATIONSmedications
ALLERGIESallergies
CARE_TEAMScare_teams
VITALSvitals
IMMUNIZATIONSimmunizations
SURGICAL_HISTORYsurgical_history
FAMILY_HISTORYfamily_history
CODING_GAPScoding_gaps

Custom Sections #

In addition to the built-in sections above, you can add fully custom sections to the chart summary. Custom sections render plugin-provided content in an iframe and are identified by a unique key. See Patient Chart Summary Custom Section Handler for details on how to implement one.

Action Buttons #

Each section of the patient chart can also be customized with action buttons. Please refer to the Action Buttons documentation for more information.

Patient Profile #

The PatientProfileConfiguration class allows you to reorder, hide, and/or specificy whether sections load expanded or collapsed.

import json

from canvas_sdk.effects import Effect, EffectType
from canvas_sdk.effects.patient_profile_configuration import PatientProfileConfiguration
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler
from logger import log


class MyHandler(BaseHandler):
    """This protocol is used to configure which sections appear in the Patient Profile section.

    The SHOW_PATIENT_PROFILE_SECTIONS payload expects a list of sections where each section is a dict like { "type": str, "start_expanded": bool }
    The accepted values for the "type" are:
    "demographics", "preferences", "preferred_pharmacies", "patient_consents",
    "care_team", "parent_guardian", "addresses", "phone_numbers", "emails", "contacts"
    """

    # Name the event type you wish to run in response to
    RESPONDS_TO = EventType.Name(EventType.PATIENT_PROFILE__SECTION_CONFIGURATION)

    def compute(self) -> list[Effect]:
        """This method gets called when an event of the type RESPONDS_TO is fired."""

        sections = [
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.PREFERENCES,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.DEMOGRAPHICS,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(
                type=PatientProfileConfiguration.Section.PREFERRED_PHARMACIES, start_expanded=True),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.PARENT_GUARDIAN,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.CONTACTS,
                                                start_expanded=True),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.CARE_TEAM,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.TELECOM,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.ADDRESSES,
                                                             start_expanded=False),
            PatientProfileConfiguration.Payload(type=PatientProfileConfiguration.Section.PATIENT_CONSENTS,
                                                start_expanded=False),
        ]

        effect = PatientProfileConfiguration(sections=sections).apply()

        return [effect]

Methods #

apply() → Effect #

Sets the order, visibility, and expansion of the patient profile sections. It is only used in response to the PATIENT_PROFILE__SECTION_CONFIGURATION event, and does nothing in any other context.

  • sections is required, with at least one section.

Attributes #

AttributeTypeDescriptionRequired
sectionslist[Payload]The sections to show, in order. Each PatientProfileConfiguration.Payload is a typed dictionary with a type (a PatientProfileConfiguration.Section) and a start_expanded boolean, which determines if the fields in that section are exposed by default.Yes

Values in the PatientProfileConfiguration.Section enum are:

ConstantDescription
DEMOGRAPHICSdemographics
PREFERENCESpreferences
PREFERRED_PHARMACIESpreferred_pharmacies
PATIENT_CONSENTSpatient_consents
CARE_TEAMcare_team
PARENT_GUARDIANparent_guardian
ADDRESSESaddresses
TELECOMtelecom
CONTACTScontacts




Panel Configuration #

This effect allows you to define which panel buttons should be displayed on the main page or the patient page.

The order of the buttons in the array will determine their order on the panel.

Before and after(width:70%)

from canvas_sdk.effects.panel_configuration import PanelConfiguration

PanelConfiguration(
  sections=[
    PanelConfiguration.PanelPatientSection.REFILL_REQUEST,
    PanelConfiguration.PanelPatientSection.LAB_REPORT,
    PanelConfiguration.PanelPatientSection.CHANGE_REQUEST,
    PanelConfiguration.PanelPatientSection.TASK,
], page=PanelConfiguration.Page.PATIENT).apply()

Methods #

apply() → Effect #

Sets the panel buttons for the page.

  • sections and page are required.
  • Every section must match the page: PanelGlobalSection values for GLOBAL, PanelPatientSection values for PATIENT.

Attributes #

AttributeTypeDescriptionRequired
sectionslist[PanelPatientSection] or list[PanelGlobalSection]List of section items, in order.Yes
pagePagePATIENT or GLOBAL.Yes

Values in the PanelGlobalSection enum are:

ConstantDescription
APPOINTMENTappointment
CHANGE_REQUESTchangeRequest
IMAGING_REPORTimagingReport
INPATIENT_STAYinpatientStay
LAB_REPORTlabReport
MESSAGEmessage
OUTSTANDING_REFERRALoutstandingReferral
PRESCRIPTION_ALERTprescriptionAlert
RECALL_APPOINTMENTrecallAppointment
REFERRAL_REPORTreferralReport
REFILL_REQUESTrefillRequest
TASKtask
UNCATEGORIZED_DOCUMENTuncategorizedDocument

Values in the PanelPatientSection enum are:

ConstantDescription
CHANGE_REQUESTchangeRequest
COMMANDcommand
IMAGING_REPORTimagingReport
INPATIENT_STAYinpatientStay
LAB_REPORTlabReport
PRESCRIPTION_ALERTprescriptionAlert
REFERRAL_REPORTreferralReport
REFILL_REQUESTrefillRequest
TASKtask
UNCATEGORIZED_DOCUMENTuncategorizedDocument

Patient Note Header Dropdown Configuration #

The PatientNoteHeaderDropdownConfiguration effect allows you to define which items appear in the dropdown menu on a patient’s note header (the triple dots at the top right of each note).

The order in the dropdown is preserved and grouped into specific sections, rather than being based on the plugin item order.

Before and after(width:60%)

from canvas_sdk.effects.patient_note_header_dropdown_configuration import PatientNoteHeaderDropdownConfiguration
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler
from canvas_sdk.effects import Effect


class NoteHeaderDropdownHandler(BaseHandler):
    RESPONDS_TO = EventType.Name(EventType.PATIENT_NOTE_HEADER_DROPDOWN__SECTION_CONFIGURATION)

    def compute(self) -> list[Effect]:
        return [PatientNoteHeaderDropdownConfiguration(items=[
            PatientNoteHeaderDropdownConfiguration.Items.PRINT_NOTE,
            PatientNoteHeaderDropdownConfiguration.Items.PRINT_SUPERBILL,
            PatientNoteHeaderDropdownConfiguration.Items.LINK_TO_PHONE,
        ]).apply()]

Methods #

apply() → Effect #

Sets the items in the note header dropdown.

  • items is required, with at least one item.

Attributes #

AttributeTypeDescriptionRequired
itemslist[Items]List of dropdown items to display.Yes

Values in the PatientNoteHeaderDropdownConfiguration.Items enum are:

ConstantDescription
LINK_TO_PHONEShow QR code to link mobile device to note
SOAPSort note sections in SOAP order (Subjective, Objective, Assessment, Plan)
APSOSort note sections in APSO order (Assessment, Plan, Subjective, Objective)
CHANGE_LOCATIONChange the note’s practice location
CHANGE_PROVIDERChange the note’s provider
CHANGE_DATE_OF_SERVICEChange the note’s date of service
PRINT_SUPERBILLPrint the superbill for billing
PRINT_ROOMING_SHEETPrint the rooming sheet for care team
PRINT_AFTER_VISIT_SUMMARYPrint the patient after visit summary
COPY_LINKCopy the note’s permalink to clipboard
PRINT_NOTEPrint the note for care team
FAX_NOTEFax the note to an external recipient
FAX_EVENT_HISTORYView fax event history for the note
MOVE_COMMANDSMove commands from this note to another note



Provider Menu Configuration #

The ProviderMenuConfiguration effect allows you to define which items appear in the provider menu (the hamburger menu at the top left of Canvas).

The effect replaces the default set of items, so every item that should stay visible has to be listed. Anything you omit is not rendered. If no installed plugin emits the effect, the menu renders unchanged.

Passing an empty list is allowed and hides every native item — useful if your plugin replaces the menu entirely with its own items.

from canvas_sdk.effects import Effect
from canvas_sdk.effects.provider_menu_configuration import ProviderMenuConfiguration
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler


class ProviderMenuHandler(BaseHandler):
    RESPONDS_TO = EventType.Name(EventType.GET_PROVIDER_MENU_CONFIGURATION)

    def compute(self) -> list[Effect]:
        return [ProviderMenuConfiguration(items=[
            ProviderMenuConfiguration.Items.PATIENTS,
            ProviderMenuConfiguration.Items.CAMPAIGNS,
            ProviderMenuConfiguration.Items.SETTINGS,
        ]).apply()]

Three things the effect does not do:

  • It does not reorder the menu. Items render in Canvas’s native order and grouping, regardless of the order you list them in.
  • It does not grant access. Permissions still apply on top, so an item you list will still render disabled for a user who lacks the permission for it.
  • It does not affect plugin-provided menu items. Applications with the provider_menu_item scope are independent of the allow-list.

The user’s avatar and name, and the Sign out button, are always rendered and cannot be hidden.

Because this is an allow-list rather than a block-list, it does not pick up native items added in future Canvas releases. If a new item ships and you want it visible, add it to your list — otherwise it stays hidden on your instance.

When the allow-list is not applied #

Canvas falls back to rendering every native item, rather than a partial or empty menu, in each of these cases:

  • No installed plugin responds to the event.
  • The plugin raises while resolving the configuration.
  • The allow-list reaches Canvas containing an item it does not recognize — the whole list is discarded, not just the unrecognized entry.

Passing something that is not an Items member raises a validation error when you construct ProviderMenuConfiguration, so most mistakes surface in your plugin before they ever reach Canvas.

If more than one installed plugin responds with a ProviderMenuConfiguration, the last effect Canvas receives wins — its allow-list replaces the earlier ones rather than merging with them.

Methods #

apply() → Effect #

Sets the native items shown in the provider menu.

  • items is required; an empty list hides every native item.

Attributes #

AttributeTypeDescriptionRequired
itemslist[Items]List of menu items to display.Yes

Values in the ProviderMenuConfiguration.Items enum are:

ConstantDescription
SCHEDULEGo to the schedule page
PATIENTSGo to the patient directory
REVENUEGo to the revenue page
POPULATIONSGo to the populations page
CAMPAIGNSGo to the campaigns page
DATA_INTEGRATIONGo to the data integration queue
QUESTIONNAIRE_BUILDERGo to the questionnaire builder
SETTINGSOpen the Canvas admin site in a new tab
MULTI_FACTOR_AUTHENTICATIONOpen multi-factor authentication setup in a new tab
CHANGELOGOpen the Canvas release notes in a new tab
HELP_CENTEROpen the Canvas help center in a new tab
GOOGLE_CALENDAROpen the Google Calendar sync page in a new tab

Google Calendar sync is a beta feature, so GOOGLE_CALENDAR renders only on instances where it is turned on.

Hiding the Schedule item #

Hiding SCHEDULE does not change where providers land after logging in — that still defaults to the schedule page. Pair the effect with a DefaultHomepageEffect so providers do not arrive on a page they can no longer navigate back to.

from canvas_sdk.effects import Effect
from canvas_sdk.effects.default_homepage import DefaultHomepageEffect
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler


class ScheduleFreeHomepage(BaseHandler):
    RESPONDS_TO = EventType.Name(EventType.GET_HOMEPAGE_CONFIGURATION)

    def compute(self) -> list[Effect]:
        return [DefaultHomepageEffect(page=DefaultHomepageEffect.Pages.PATIENTS).apply()]

Hiding SCHEDULE also leaves the Appointments filter in the side panel in place. Removing the scheduling experience end to end means coordinating three independent controls: this effect for the menu item, PanelConfiguration for the Appointments filter, and DefaultHomepageEffect for the landing page.

Omitting SETTINGS or MULTI_FACTOR_AUTHENTICATION hides the links to the admin site and to multi-factor authentication setup, so make sure your users have another route to them if they need one.



Modals #

The LaunchModalEffect class allows you to launch modals in Canvas, providing a flexible way to display content or navigate to external resources.

Methods #

apply() → Effect #

Launches the modal for the user whose action triggered the handler. See Where Modals Open.

  • Set at most one of url or content; setting both raises an error.

Attributes #

AttributeTypeDescriptionRequired
urlstrThe URL to load within the modal.No
contentstrContent to display directly within the modal.No
targetTargetTypeWhere the modal is launched; see targets. Defaults to DEFAULT_MODAL.No
dismissibleboolWhether the user can close a DEFAULT_MODAL themselves. Defaults to True. Set it to False for a blocking modal that only your application can close.No
titlestrThe title of the modal, displayed when minimized. Defaults to Untitled.No

Targets #

LaunchModalEffect.TargetType values:

  • DEFAULT_MODAL: Opens the URL in a modal centered on the screen.
  • NEW_WINDOW: Opens the content in a new browser window.
  • RIGHT_CHART_PANE: Opens the URL in the right-hand pane of the patient chart.
  • RIGHT_CHART_PANE_LARGE: Like above, but a bit wider. A right chart pane opened in a patient’s chart stays open while the user moves around that chart. This includes the chart, Profile, Documents, application tabs, and diagnostics. The content inside the pane isn’t reloaded, so it keeps its state. The pane closes when the user opens a different patient or leaves the chart.
  • PAGE: Opens the content as a full page.
  • NOTE: Opens the content within a note tab (used with Note Applications).
  • DOCKED_PANE: Opens the content in a persistent pane pinned to an edge of the window. This target is returned by a Docked Application, which sets DOCK_EDGE and DOCK_SIZE.

Example #

from canvas_sdk.effects.launch_modal import LaunchModalEffect

class ModalEffectHandler:
    def compute(self):
        modal_effect = LaunchModalEffect(
            url="https://example.com/info",
            content=None,
            target=LaunchModalEffect.TargetType.DEFAULT_MODAL,
            title="Example Info"
        )
        return [modal_effect.apply()]

Where Modals Open #

A modal opens for the user whose action triggered the handler, in the app they’re using: Canvas or the patient portal.

An application’s on_open, an action button click, and the PATIENT_PORTAL__POST_LOGIN event return the modal in their own response, so it opens right away. Canvas pushes a modal from any other handler, such as a SimpleAPI route, to that user’s browser. The modal opens only if the user has Canvas or the portal open at that moment. If the event has no acting user, the modal doesn’t open.

In the patient portal, use the DEFAULT_MODAL target. The exception is a portal application’s on_open. It can return the PAGE target to show content as the application’s page, as in the patient portal application example. The portal ignores a pushed modal with the PAGE target.

Closing Modals from Applications #

When building applications with the Canvas SDK, you may encounter scenarios where you need to programmatically dismiss modals. This can be particularly useful in automated testing or when creating user flows that require closing modals based on certain conditions.

Here’s a simple example of how to dismiss modals from your applications using JavaScript.

<script>
    let messagePort = null;

    // Listen for the port transfer from the Canvas Application
    window.addEventListener('message', (event) => {
      // Check if this is the INIT_CHANNEL message with a port
      if (event.data?.type === 'INIT_CHANNEL' && event.ports[0]) {

        // Store the port for later use
        messagePort = event.ports[0];
        messagePort.start();
        messagePort.postMessage({ type: 'CLOSE_MODAL' });
      }
    });
</script>

And that’s it! This script establishes a communication channel with the Canvas Application by listening for the INIT_CHANNEL event, capturing the message port, and then sending a CLOSE_MODAL message through that port to close any open modals when the application loads. You can customize the event listener to trigger the modal dismissal based on your specific requirements.

While developers might find odd to be sending a message to themselves, this is the current method supported by the Canvas SDK for dismissing modals, in order to avoid potential security issues with cross-origin messaging and flooding the main application with messages.

This twist on the Holywood Principle ensures that your application remains secure while still providing the functionality needed to manage modals effectively.

Blocking Modals #

Use a blocking modal when the user has to finish a step before moving on, such as signing a consent form. Set dismissible=False:

from canvas_sdk.effects.launch_modal import LaunchModalEffect

modal_effect = LaunchModalEffect(
    url="https://example.com/consent",
    target=LaunchModalEffect.TargetType.DEFAULT_MODAL,
    title="Consent",
    dismissible=False,
)

A blocking modal stays open when the user:

  • Presses Escape or selects the backdrop.
  • Navigates to another page or selects the browser’s Back button.

On small screens, a blocking modal doesn’t show the back arrow. Signing out of Canvas or the patient portal closes it.



Resizing Modals #

Modal overlays can now be dynamically resized by embedded applications using the MessageChannel API. Applications launching with a DEFAULT_MODAL target can send a RESIZE message to adjust the modal’s width and/or height:

<script>
    let messagePort = null;

    // Listen for the port transfer from the Canvas Application
    window.addEventListener('message', (event) => {
      // Check if this is the INIT_CHANNEL message with a port
      if (event.data?.type === 'INIT_CHANNEL' && event.ports?.[0]) {

        // Store the port for later use
        messagePort = event.ports[0];
        messagePort.start();
        // Example: Resize modal to specific dimensions
        messagePort.postMessage({
          type: 'RESIZE',
          width: 800,  // pixels
          height: 600  // pixels
        });
      }
    });
</script>

This enables embedded applications to optimize their display area based on content requirements, improving the user experience for dynamic or responsive plugin interfaces.

Custom HTML and Django Templates #

Every LaunchModalEffect target, from a modal or side pane to a full page, and every application that opens one, renders its content in one of two ways: load a page in an iframe by setting the effect’s url (see Implementing an Application), or pass HTML in content, for example rendered from a Django template with render_to_string. Either can be a single-page application such as React: serve the page from your plugin and point url at it, or include the app’s scripts in the HTML you pass as content.

To facilitate the use of custom HTML, you can utilize the render_to_string utility from canvas_sdk.templates to render Django templates with a specified context. This allows for dynamic rendering of HTML that can be passed to a LaunchModalEffect or PortalWidget.

from typing import Any

def render_to_string(template_name: str, context: dict[str, Any] | None = None) -> str | None:
    """Load a template and render it with the given context.

    Args:
        template_name (str): The path to the template file, relative to the plugin package.
            If the path starts with a forward slash ("/"), it will be stripped during resolution.
        context (dict[str, Any] | None): A dictionary of variables to pass to the template
            for rendering. Defaults to None, which uses an empty context.

    Returns:
        str: The rendered template as a string.

    Raises:
        FileNotFoundError: If the template file does not exist within the plugin's directory
            or if the resolved path is invalid.
    """

Example Template #

Consider a simple HTML file named templates/custom_content.html:

<!DOCTYPE html>
<html>
  <head>
    <title>{{ title }}</title>
  </head>
  <body>
    <h1>{{ heading }}</h1>
    <p>{{ message }}</p>
  </body>
</html>

This template uses Django template placeholders like {{ title }}, {{ heading }}, and {{ message }} to dynamically render content based on the provided context.

Rendering the Template in Python #

Here’s how you can use the render_to_string utility to render the template and pass the resulting HTML to a LaunchModalEffect or PortalWidget:

from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.effects.widgets import PortalWidget
from canvas_sdk.templates import render_to_string

class ModalEffectHandler:
    def compute(self):
        # Define the context for the template
        context = {
            "title": "Welcome Modal",
            "heading": "Hello, User!",
            "message": "This is a dynamically rendered modal using Django templates."
        }

        # Render the HTML content using the template and context
        rendered_html = render_to_string("templates/custom_content.html", context)

        # Create a LaunchModalEffect with the rendered content
        modal_effect = LaunchModalEffect(
            content=rendered_html,
            target=LaunchModalEffect.TargetType.DEFAULT_MODAL
        )

        return [modal_effect.apply()]

class PortalWidgetHandler:
    def compute(self):
        # Define the context for the template
        context = {
            "title": "Welcome Modal",
            "heading": "Hello, User!",
            "message": "This is a dynamically rendered modal using Django templates."
        }

        # Render the HTML content using the template and context
        rendered_html = render_to_string("templates/custom_content.html", context)

        # Create a PortalWidget with the rendered content
        portal_widget = PortalWidget(
            content=rendered_html,
            size=PortalWidget.Size.COMPACT,
            priority=25
        )

        return [portal_widget.apply()]

Additional Configuration #

To use URLs or custom scripts within the LaunchModalEffect or PortalWidget, additional security configurations must be specified in the CANVAS_MANIFEST.json file of your plugin.

  • Allowing URLs: URLs specified in the url property must be added to the url_permissions section of the CANVAS_MANIFEST.json in order for the URL to load properly.
  • Allowing custom scripts: If you need to load scripts from an external source, the URL for the script must be added to the url_permissions section of the CANVAS_MANIFEST.json and 'SCRIPTS' must be in the permissions list.
  • Requesting microphone access: If the site in your modal or widget needs microphone access, 'MICROPHONE' must be in the URL’s permissions list.
  • Requesting camera access: If the site in your modal or widget needs camera access, 'CAMERA' must be in the URL’s permissions list.
  • Requesting clipboard read access: If the site in your modal or widget needs to read from the user’s clipboard, 'CLIPBOARD_READ' must be in the URL’s permissions list.
  • Requesting clipboard write access: If the site in your modal or widget needs to write to the user’s clipboard, 'CLIPBOARD_WRITE' must be in the URL’s permissions list.
  • Allowing browser access to cookies from the iframe’s origin: If you want the loaded URL to access cookies for its domain, 'ALLOW_SAME_ORIGIN' must be in the URL’s permissions list. If the URL you’re loading requires authentication, this will prevent your user from having to log in each time the modal is launched.

The URLs must match the format available here.

{
  "sdk_version": "0.1.4",
  "plugin_version": "0.0.1",
  "name": "custom_html",
  "description": "...",
  "url_permissions": [
    {
      "url": "https://example.com/info",
      "permissions": ["ALLOW_SAME_ORIGIN", "MICROPHONE", "CAMERA", "CLIPBOARD_READ", "CLIPBOARD_WRITE"]
    },
    {
      "url": "https://d3js.org/d3.v4.js",
      "permissions": ["SCRIPTS"]
    }
  ]
}

The iframe sandbox and ALLOW_SAME_ORIGIN #

Whether an application’s iframe gets a sandbox attribute depends on the url_permissions entry its URL matches:

  • With ALLOW_SAME_ORIGIN, the iframe is sandboxed as allow-same-origin allow-forms allow-popups allow-scripts. Popups work, so window.open(url, '_blank') and popup-based sign-in flows are fine. Navigating the top window from inside the iframe is blocked.
  • Without a matching entry that grants it, the iframe has no sandbox attribute, and top-window navigation works.

URLs match by case-insensitive prefix, and every character counts, including the scheme, port, and trailing slash. An entry of https://example.com/ does not match a page at https://example.com. If top-window navigation works in one environment but not another, the URLs are matching url_permissions differently. To navigate away while keeping ALLOW_SAME_ORIGIN, open the page with window.open(url, '_blank'), or remove ALLOW_SAME_ORIGIN if your application doesn’t need it.