diff --git a/README.md b/README.md index d6adb437..aa0e7527 100644 --- a/README.md +++ b/README.md @@ -1947,6 +1947,7 @@ type Options = { enableUnknownActivation: boolean; isEuIterableService: boolean; dangerouslyAllowJsPopups: boolean; + allowIframeScripts: boolean; eventThresholdLimit?: number; onUnknownUserCreated?: (userId: string) => void; identityResolution?: { @@ -2698,6 +2699,43 @@ For more information, see: - [MDN docs for `allow-popups-to-escape-sandbox`](https://developer.mozilla.org/docs/Web/HTML/Element/iframe#allow-popups-to-escape-sandbox) - [Can I Use? `allow-popups-to-escape-sandbox`](https://caniuse.com/mdn-html_elements_iframe_sandbox_allow-popups-to-escape-sandbox) +### Safari: Enabling full in-app message functionality + +By default, Safari blocks JavaScript execution inside the sandboxed `iframe` +used to display in-app messages. This prevents click tracking, `action://` +link support, and proper close button behavior in Safari. + +To enable full functionality in Safari, set `allowIframeScripts` to `true` +in the configuration options. This adds `allow-scripts` to the iframe sandbox +attribute, allowing Safari to execute JavaScript event handlers inside the +iframe just like Chrome and Firefox. + +```ts +import { initializeWithConfig } from '@iterable/web-sdk'; + +const { clearRefresh, setEmail, setUserID, logout } = initializeWithConfig({ + authToken: '<>', + configOptions: { + allowIframeScripts: true, + }, + generateJWT: ({ email, userID }) => + yourAsyncJWTGeneratorMethod({ email, userID }).then( + ({ jwt_token }) => jwt_token + ) +}); +``` + +When this option is enabled: + +- **Click tracking** works in Safari (previously not tracked) +- **`action://` links** work in Safari (previously non-functional) +- **Close buttons** render inside the iframe as expected +- **`isRequiredToDismissMessage`** is properly honored in Safari + +Since in-app message content is authored by you through the Iterable platform, +the security risk of enabling scripts is minimal. However, if your messages +include user-generated content, evaluate the risk before enabling this option. + # TypeScript Iterable's Web SDK includes TypeScript definitions. All SDK methods should be diff --git a/src/inapp/inapp.ts b/src/inapp/inapp.ts index 1c954925..27897830 100644 --- a/src/inapp/inapp.ts +++ b/src/inapp/inapp.ts @@ -19,6 +19,7 @@ import { trackInAppOpen } from '../events/inapp/events'; import { IterablePromise } from '../types'; +import { config } from '../utils/config'; import { requestMessages } from './request'; import { DisplayOptions, @@ -274,8 +275,12 @@ export function getInAppMessages( } const ua = navigator.userAgent; - const isSafari = + const isSafariUA = !!ua.match(/safari/i) && !ua.match(/chrome|chromium|crios/i); + // When allowIframeScripts is enabled, Safari can execute JS in iframes + // so we don't need the Safari-specific workarounds + const isSafari = + isSafariUA && !config.getConfig('allowIframeScripts'); /** * We allow users to dismiss messages by clicking outside of the diff --git a/src/inapp/utils.ts b/src/inapp/utils.ts index 645a9a2d..ee0a3ed3 100644 --- a/src/inapp/utils.ts +++ b/src/inapp/utils.ts @@ -288,13 +288,15 @@ const generateSecuredIFrame = () => { iframe.setAttribute('id', 'iterable-iframe'); // allow-popups and allow-top-navigation is to enable links for Safari since the iframe will block // event handlers on elements in it preventing our custom link handling + // allow-scripts is optionally enabled via allowIframeScripts config to support + // click tracking and action:// links in Safari iframe.setAttribute( 'sandbox', `allow-same-origin allow-popups allow-top-navigation ${ config.getConfig('dangerouslyAllowJsPopups') ? 'allow-popups-to-escape-sandbox' : '' - }` + } ${config.getConfig('allowIframeScripts') ? 'allow-scripts' : ''}`.trim() ); /* _display: none_ would remove the ability to set event handlers on elements diff --git a/src/utils/config.ts b/src/utils/config.ts index 28285ebc..29ff0583 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -11,6 +11,7 @@ export type Options = { enableUnknownActivation: boolean; isEuIterableService: boolean; dangerouslyAllowJsPopups: boolean; + allowIframeScripts: boolean; eventThresholdLimit?: number; onUnknownUserCreated?: (userId: string) => void; identityResolution?: IdentityResolution; @@ -23,6 +24,7 @@ const _config = () => { enableUnknownActivation: false, isEuIterableService: false, dangerouslyAllowJsPopups: false, + allowIframeScripts: false, eventThresholdLimit: DEFAULT_EVENT_THRESHOLD_LIMIT, identityResolution: { replayOnVisitorToKnown: true,