From 6f210e8cd87c641fb70f99636fcc53ac86fc1c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Fri, 6 Nov 2026 14:04:53 +0000 Subject: [PATCH 1/5] test(SDK-615): add IterableInboxMessageDisplay link handling tests Align reserved iterable:// handling so unknown schemes do not reach urlHandler; only delete and dismiss run SDK actions. --- CHANGELOG.md | 2 + .../IterableInboxMessageDisplay.test.tsx | 251 ++++++++++++++++++ .../IterableInboxMessageDisplay.tsx | 3 + 3 files changed, 256 insertions(+) create mode 100644 src/inbox/components/IterableInboxMessageDisplay.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a82ae61a..34202ba6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,13 @@ ### Fixes +- Inbox message HTML links: unknown `iterable://` URLs no longer fall through to `urlHandler`; only `iterable://delete` and `iterable://dismiss` are handled as SDK actions (SDK-615, SDK-633). - Fixed `Iterable.trackPushOpenWithCampaignId` on Android so the `appAlreadyRunning` argument is forwarded to the native SDK instead of always being sent as `false`. - `Iterable.inAppManager.showMessage` now rejects on iOS when the message id is empty or no longer in the native queue, matching Android. Previously the promise never settled. ### Updates +- Added unit tests for inbox message link handling in `IterableInboxMessageDisplay` (SDK-615). - Added optional placement IDs to `Iterable.embeddedManager.syncMessages()` so apps can sync embedded messages for specific placements only (SDK-751). When omitted, null, or empty, behavior matches the previous sync-all call. - **Android:** Scoped sync (`syncMessages([id, …])`) matches the pinned iterableapi behavior and may remove cached messages for placements not included in the sync response (SDK-798). iOS preserves unrequested placements. Prefer `syncMessages()` without placement IDs on Android until the native fix ships and the Android SDK pin is bumped. - Added `IterableInAppMessage.jsonOnly` so `inAppHandler` and in-app message fetches can distinguish JSON-only messages from HTML in-apps (SDK-750). diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx new file mode 100644 index 000000000..04e3d7c0b --- /dev/null +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -0,0 +1,251 @@ +import { act, fireEvent, render, waitFor } from '@testing-library/react-native'; + +import { MockLinking } from '../../__mocks__/MockLinking'; +import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; +import { Iterable } from '../../core/classes/Iterable'; +import { IterableActionContext } from '../../core/classes/IterableActionContext'; +import { IterableConfig } from '../../core/classes/IterableConfig'; +import { IterableEdgeInsets } from '../../core/classes/IterableEdgeInsets'; +import { IterableActionSource } from '../../core/enums/IterableActionSource'; +import { IterableHtmlInAppContent } from '../../inApp/classes/IterableHtmlInAppContent'; +import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import { IterableInAppTrigger } from '../../inApp/classes/IterableInAppTrigger'; +import { IterableInboxMetadata } from '../../inApp/classes/IterableInboxMetadata'; +import { IterableInAppTriggerType } from '../../inApp/enums/IterableInAppTriggerType'; +import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import type { IterableInboxRowViewModel } from '../types'; +import { IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; + +jest.mock('react-native-webview', () => { + const React = require('react'); + const { View } = require('react-native'); + const WebView = (props: { + onMessage?: (event: { nativeEvent: { data: string } }) => void; + }) => + React.createElement(View, { + testID: 'inbox-message-webview', + onMessage: props.onMessage, + }); + return { WebView }; +}); + +describe('IterableInboxMessageDisplay', () => { + const messageId = 'msg-1'; + + const rowViewModel: IterableInboxRowViewModel = { + title: 'T', + read: false, + inAppMessage: new IterableInAppMessage( + messageId, + 1, + new IterableInAppTrigger(IterableInAppTriggerType.never), + undefined, + undefined, + true, + new IterableInboxMetadata('Hello', undefined, undefined), + undefined, + false, + 0 + ), + }; + + const htmlContent = new IterableHtmlInAppContent( + new IterableEdgeInsets(0, 0, 0, 0), + '' + ); + + let deleteRow: jest.Mock; + let returnToInbox: jest.Mock; + let urlHandler: jest.Mock; + let customActionHandler: jest.Mock; + let trackInAppClickSpy: jest.SpyInstance; + let trackInAppCloseSpy: jest.SpyInstance; + + beforeEach(() => { + deleteRow = jest.fn(); + returnToInbox = jest.fn((callback?: () => void) => { + callback?.(); + }); + urlHandler = jest.fn(); + customActionHandler = jest.fn(); + + const config = new IterableConfig(); + config.urlHandler = urlHandler; + config.customActionHandler = customActionHandler; + Iterable.savedConfig = config; + + trackInAppClickSpy = jest + .spyOn(Iterable, 'trackInAppClick') + .mockImplementation(() => {}); + trackInAppCloseSpy = jest + .spyOn(Iterable, 'trackInAppClose') + .mockImplementation(() => {}); + + MockRNIterableAPI.trackInAppClick.mockClear(); + MockRNIterableAPI.trackInAppClose.mockClear(); + MockLinking.openURL.mockClear(); + }); + + afterEach(() => { + trackInAppClickSpy.mockRestore(); + trackInAppCloseSpy.mockRestore(); + Iterable.savedConfig = new IterableConfig(); + }); + + async function renderWithContent() { + const utils = render( + + ); + await act(async () => { + await Promise.resolve(); + }); + return utils; + } + + function postMessage( + getByTestId: (id: string) => { + props: { onMessage?: (e: { nativeEvent: { data: string } }) => void }; + }, + url: string + ) { + const webView = getByTestId('inbox-message-webview'); + const onMessage = webView.props.onMessage; + expect(onMessage).toBeDefined(); + onMessage!({ nativeEvent: { data: url } }); + } + + it('should open http(s) in the system browser and return to the inbox', async () => { + const { getByTestId } = await renderWithContent(); + const url = 'https://example.com/path'; + + postMessage(getByTestId, url); + + expect(trackInAppClickSpy).toHaveBeenCalledWith( + rowViewModel.inAppMessage, + IterableInAppLocation.inbox, + url + ); + expect(trackInAppCloseSpy).toHaveBeenCalledWith( + rowViewModel.inAppMessage, + IterableInAppLocation.inbox, + IterableInAppCloseSource.link, + url + ); + expect(returnToInbox).toHaveBeenCalled(); + expect(MockLinking.openURL).toHaveBeenCalledWith(url); + expect(deleteRow).not.toHaveBeenCalled(); + expect(urlHandler).not.toHaveBeenCalled(); + }); + + it('should delete the row and return to the inbox for iterable://delete', async () => { + const { getByTestId } = await renderWithContent(); + + postMessage(getByTestId, 'iterable://delete'); + + expect(deleteRow).toHaveBeenCalledWith(messageId); + expect(returnToInbox).toHaveBeenCalled(); + expect(urlHandler).not.toHaveBeenCalled(); + expect(MockLinking.openURL).not.toHaveBeenCalled(); + }); + + it('should return to the inbox for iterable://dismiss without deleting', async () => { + const { getByTestId } = await renderWithContent(); + + postMessage(getByTestId, 'iterable://dismiss'); + + expect(returnToInbox).toHaveBeenCalled(); + expect(deleteRow).not.toHaveBeenCalled(); + expect(urlHandler).not.toHaveBeenCalled(); + }); + + it('should invoke customActionHandler with the action type for action:// URLs', async () => { + const { getByTestId } = await renderWithContent(); + const url = 'action://foo'; + + postMessage(getByTestId, url); + + await waitFor(() => { + expect(customActionHandler).toHaveBeenCalled(); + }); + + expect(customActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ type: 'foo' }), + expect.any(IterableActionContext) + ); + const [, context] = customActionHandler.mock.calls[0]; + expect(context.source).toBe(IterableActionSource.inApp); + expect(returnToInbox).toHaveBeenCalled(); + expect(urlHandler).not.toHaveBeenCalled(); + }); + + it('should swallow unknown iterable:// URLs without calling urlHandler or returning to inbox', async () => { + const { getByTestId } = await renderWithContent(); + + postMessage(getByTestId, 'iterable://foo'); + + expect(returnToInbox).not.toHaveBeenCalled(); + expect(urlHandler).not.toHaveBeenCalled(); + expect(MockLinking.openURL).not.toHaveBeenCalled(); + expect(deleteRow).not.toHaveBeenCalled(); + }); + + it('should pass other URLs to urlHandler', async () => { + const { getByTestId } = await renderWithContent(); + const url = 'myapp://screen'; + + postMessage(getByTestId, url); + + await waitFor(() => { + expect(urlHandler).toHaveBeenCalledWith(url, expect.any(IterableActionContext)); + }); + expect(returnToInbox).toHaveBeenCalled(); + expect(MockLinking.openURL).not.toHaveBeenCalled(); + }); + + it('should forward click tracking to the native bridge', async () => { + trackInAppClickSpy.mockRestore(); + trackInAppCloseSpy.mockRestore(); + + const { getByTestId } = await renderWithContent(); + const url = 'https://example.com/x'; + + postMessage(getByTestId, url); + + expect(MockRNIterableAPI.trackInAppClick).toHaveBeenCalledWith( + messageId, + IterableInAppLocation.inbox, + url + ); + expect(MockRNIterableAPI.trackInAppClose).toHaveBeenCalledWith( + messageId, + IterableInAppLocation.inbox, + IterableInAppCloseSource.link, + url + ); + }); + + it('should track close when the back button is pressed', async () => { + trackInAppClickSpy.mockRestore(); + trackInAppCloseSpy.mockRestore(); + + const { getByText } = await renderWithContent(); + + fireEvent.press(getByText('Inbox')); + + expect(returnToInbox).toHaveBeenCalled(); + expect(MockRNIterableAPI.trackInAppClose).toHaveBeenCalledWith( + messageId, + IterableInAppLocation.inbox, + IterableInAppCloseSource.back, + undefined + ); + }); +}); diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index d42306a04..32459acf2 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -181,6 +181,9 @@ export const IterableInboxMessageDisplay = ({ //handle dismiss action } else if (URL === 'iterable://dismiss') { returnToInbox(); + // iterable:// is reserved — only delete/dismiss are SDK actions (SDK-633) + } else if (URL.startsWith('iterable://')) { + return; //handle external link } else if (URL.slice(0, 4) === 'http') { returnToInbox(() => Linking.openURL(URL)); From 90f6c021ef213a153044526f9dd062adac1d9057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Fri, 6 Nov 2026 15:10:40 +0000 Subject: [PATCH 2/5] test(SDK-615): rely on global WebView mock in inbox display tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the file-local jest.mock so we use jest.setup’s View stub; find the WebView node by View type + onMessage. --- .../IterableInboxMessageDisplay.test.tsx | 73 +++++++++---------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx index 04e3d7c0b..09720d654 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.test.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -1,4 +1,11 @@ -import { act, fireEvent, render, waitFor } from '@testing-library/react-native'; +import { View } from 'react-native'; +import { + act, + fireEvent, + render, + type RenderResult, + waitFor, +} from '@testing-library/react-native'; import { MockLinking } from '../../__mocks__/MockLinking'; import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; @@ -17,19 +24,6 @@ import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; import type { IterableInboxRowViewModel } from '../types'; import { IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; -jest.mock('react-native-webview', () => { - const React = require('react'); - const { View } = require('react-native'); - const WebView = (props: { - onMessage?: (event: { nativeEvent: { data: string } }) => void; - }) => - React.createElement(View, { - testID: 'inbox-message-webview', - onMessage: props.onMessage, - }); - return { WebView }; -}); - describe('IterableInboxMessageDisplay', () => { const messageId = 'msg-1'; @@ -93,7 +87,7 @@ describe('IterableInboxMessageDisplay', () => { Iterable.savedConfig = new IterableConfig(); }); - async function renderWithContent() { + async function renderWithContent(): Promise { const utils = render( { return utils; } - function postMessage( - getByTestId: (id: string) => { - props: { onMessage?: (e: { nativeEvent: { data: string } }) => void }; - }, - url: string - ) { - const webView = getByTestId('inbox-message-webview'); - const onMessage = webView.props.onMessage; - expect(onMessage).toBeDefined(); - onMessage!({ nativeEvent: { data: url } }); + /** Global setup mocks `WebView` as `View`; pick the node wired with `onMessage`. */ + function postWebViewMessage(utils: RenderResult, url: string) { + const candidates = utils.UNSAFE_getAllByType(View); + const webView = candidates.find( + (node) => typeof node.props.onMessage === 'function' + ); + expect(webView).toBeDefined(); + const onMessage = webView!.props.onMessage as ( + e: { nativeEvent: { data: string } } + ) => void; + onMessage({ nativeEvent: { data: url } }); } it('should open http(s) in the system browser and return to the inbox', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); const url = 'https://example.com/path'; - postMessage(getByTestId, url); + postWebViewMessage(utils, url); expect(trackInAppClickSpy).toHaveBeenCalledWith( rowViewModel.inAppMessage, @@ -146,9 +141,9 @@ describe('IterableInboxMessageDisplay', () => { }); it('should delete the row and return to the inbox for iterable://delete', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); - postMessage(getByTestId, 'iterable://delete'); + postWebViewMessage(utils, 'iterable://delete'); expect(deleteRow).toHaveBeenCalledWith(messageId); expect(returnToInbox).toHaveBeenCalled(); @@ -157,9 +152,9 @@ describe('IterableInboxMessageDisplay', () => { }); it('should return to the inbox for iterable://dismiss without deleting', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); - postMessage(getByTestId, 'iterable://dismiss'); + postWebViewMessage(utils, 'iterable://dismiss'); expect(returnToInbox).toHaveBeenCalled(); expect(deleteRow).not.toHaveBeenCalled(); @@ -167,10 +162,10 @@ describe('IterableInboxMessageDisplay', () => { }); it('should invoke customActionHandler with the action type for action:// URLs', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); const url = 'action://foo'; - postMessage(getByTestId, url); + postWebViewMessage(utils, url); await waitFor(() => { expect(customActionHandler).toHaveBeenCalled(); @@ -187,9 +182,9 @@ describe('IterableInboxMessageDisplay', () => { }); it('should swallow unknown iterable:// URLs without calling urlHandler or returning to inbox', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); - postMessage(getByTestId, 'iterable://foo'); + postWebViewMessage(utils, 'iterable://foo'); expect(returnToInbox).not.toHaveBeenCalled(); expect(urlHandler).not.toHaveBeenCalled(); @@ -198,10 +193,10 @@ describe('IterableInboxMessageDisplay', () => { }); it('should pass other URLs to urlHandler', async () => { - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); const url = 'myapp://screen'; - postMessage(getByTestId, url); + postWebViewMessage(utils, url); await waitFor(() => { expect(urlHandler).toHaveBeenCalledWith(url, expect.any(IterableActionContext)); @@ -214,10 +209,10 @@ describe('IterableInboxMessageDisplay', () => { trackInAppClickSpy.mockRestore(); trackInAppCloseSpy.mockRestore(); - const { getByTestId } = await renderWithContent(); + const utils = await renderWithContent(); const url = 'https://example.com/x'; - postMessage(getByTestId, url); + postWebViewMessage(utils, url); expect(MockRNIterableAPI.trackInAppClick).toHaveBeenCalledWith( messageId, From 5caf58e6d8925b9f2a1711b8236345f79efca724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Fri, 6 Nov 2026 15:36:49 +0000 Subject: [PATCH 3/5] docs(SDK-615): note iterable click tracking matches native Comment why trackInAppClick/Close run before unknown iterable:// handling. --- src/inbox/components/IterableInboxMessageDisplay.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 32459acf2..d72200ab0 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -163,6 +163,9 @@ export const IterableInboxMessageDisplay = ({ const source = IterableActionSource.inApp; const context = new IterableActionContext(action, source); + // Track before routing: aligns with native HTML in-app (click/close first, + // then iterable:// handling). Unknown iterable:// paths still get these + // events while skipping urlHandler / returnToInbox below. Iterable.trackInAppClick( rowViewModel.inAppMessage, IterableInAppLocation.inbox, From ac5713524c0f6d9beab7d2d16f1f0efbf44547e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Sun, 8 Nov 2026 17:13:42 +0000 Subject: [PATCH 4/5] fix(SDK-615): close inbox on unknown iterable:// after track Align with native HTML in-apps; strengthen tests and changelog per review. --- CHANGELOG.md | 3 +-- .../IterableInboxMessageDisplay.test.tsx | 18 +++++++++++++++--- .../components/IterableInboxMessageDisplay.tsx | 11 ++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34202ba6f..8948afbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,12 @@ ### Fixes -- Inbox message HTML links: unknown `iterable://` URLs no longer fall through to `urlHandler`; only `iterable://delete` and `iterable://dismiss` are handled as SDK actions (SDK-615, SDK-633). +- Fixed inbox HTML messages forwarding unknown `iterable://` links to `urlHandler`. `iterable://` is reserved for `iterable://delete` and `iterable://dismiss`; any other `iterable://` link now closes the message without calling a handler. Use `action://name` for custom actions (`customActionHandler`) and `https` or your app's own scheme for deep links (`urlHandler`). - Fixed `Iterable.trackPushOpenWithCampaignId` on Android so the `appAlreadyRunning` argument is forwarded to the native SDK instead of always being sent as `false`. - `Iterable.inAppManager.showMessage` now rejects on iOS when the message id is empty or no longer in the native queue, matching Android. Previously the promise never settled. ### Updates -- Added unit tests for inbox message link handling in `IterableInboxMessageDisplay` (SDK-615). - Added optional placement IDs to `Iterable.embeddedManager.syncMessages()` so apps can sync embedded messages for specific placements only (SDK-751). When omitted, null, or empty, behavior matches the previous sync-all call. - **Android:** Scoped sync (`syncMessages([id, …])`) matches the pinned iterableapi behavior and may remove cached messages for placements not included in the sync response (SDK-798). iOS preserves unrequested placements. Prefer `syncMessages()` without placement IDs on Android until the native fix ships and the Android SDK pin is bumped. - Added `IterableInAppMessage.jsonOnly` so `inAppHandler` and in-app message fetches can distinguish JSON-only messages from HTML in-apps (SDK-750). diff --git a/src/inbox/components/IterableInboxMessageDisplay.test.tsx b/src/inbox/components/IterableInboxMessageDisplay.test.tsx index 09720d654..a936e0139 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.test.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.test.tsx @@ -148,6 +148,7 @@ describe('IterableInboxMessageDisplay', () => { expect(deleteRow).toHaveBeenCalledWith(messageId); expect(returnToInbox).toHaveBeenCalled(); expect(urlHandler).not.toHaveBeenCalled(); + expect(customActionHandler).not.toHaveBeenCalled(); expect(MockLinking.openURL).not.toHaveBeenCalled(); }); @@ -159,6 +160,7 @@ describe('IterableInboxMessageDisplay', () => { expect(returnToInbox).toHaveBeenCalled(); expect(deleteRow).not.toHaveBeenCalled(); expect(urlHandler).not.toHaveBeenCalled(); + expect(customActionHandler).not.toHaveBeenCalled(); }); it('should invoke customActionHandler with the action type for action:// URLs', async () => { @@ -181,13 +183,23 @@ describe('IterableInboxMessageDisplay', () => { expect(urlHandler).not.toHaveBeenCalled(); }); - it('should swallow unknown iterable:// URLs without calling urlHandler or returning to inbox', async () => { + it('should close the message for unknown iterable:// URLs without calling any handler', async () => { const utils = await renderWithContent(); + const url = 'iterable://foo'; - postWebViewMessage(utils, 'iterable://foo'); + postWebViewMessage(utils, url); - expect(returnToInbox).not.toHaveBeenCalled(); + expect(returnToInbox).toHaveBeenCalledTimes(1); + expect(trackInAppClickSpy).toHaveBeenCalledTimes(1); + expect(trackInAppCloseSpy).toHaveBeenCalledTimes(1); + expect(trackInAppCloseSpy).toHaveBeenCalledWith( + rowViewModel.inAppMessage, + IterableInAppLocation.inbox, + IterableInAppCloseSource.link, + url + ); expect(urlHandler).not.toHaveBeenCalled(); + expect(customActionHandler).not.toHaveBeenCalled(); expect(MockLinking.openURL).not.toHaveBeenCalled(); expect(deleteRow).not.toHaveBeenCalled(); }); diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index d72200ab0..549d344df 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -163,9 +163,9 @@ export const IterableInboxMessageDisplay = ({ const source = IterableActionSource.inApp; const context = new IterableActionContext(action, source); - // Track before routing: aligns with native HTML in-app (click/close first, - // then iterable:// handling). Unknown iterable:// paths still get these - // events while skipping urlHandler / returnToInbox below. + // Track before routing, like native HTML in-apps: every link click sends + // click and close(link), then the message closes. Unknown iterable:// + // paths close without calling urlHandler or customActionHandler. Iterable.trackInAppClick( rowViewModel.inAppMessage, IterableInAppLocation.inbox, @@ -184,9 +184,10 @@ export const IterableInboxMessageDisplay = ({ //handle dismiss action } else if (URL === 'iterable://dismiss') { returnToInbox(); - // iterable:// is reserved — only delete/dismiss are SDK actions (SDK-633) + // iterable:// is reserved: only delete/dismiss are SDK actions (SDK-633). + // Any other path closes the message without calling a handler, like native. } else if (URL.startsWith('iterable://')) { - return; + returnToInbox(); //handle external link } else if (URL.slice(0, 4) === 'http') { returnToInbox(() => Linking.openURL(URL)); From 5c42a462c632eee68564ece4da07a570274f1ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferr=C3=A3o?= Date: Wed, 11 Nov 2026 10:48:43 +0000 Subject: [PATCH 5/5] docs(SDK-615): clarify inbox link changelog and comment wording Per review: http(s) uses Linking not urlHandler in inbox; comment describes outcome without claiming cross-platform track order. --- CHANGELOG.md | 2 +- src/inbox/components/IterableInboxMessageDisplay.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8948afbae..b1835b984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Fixes -- Fixed inbox HTML messages forwarding unknown `iterable://` links to `urlHandler`. `iterable://` is reserved for `iterable://delete` and `iterable://dismiss`; any other `iterable://` link now closes the message without calling a handler. Use `action://name` for custom actions (`customActionHandler`) and `https` or your app's own scheme for deep links (`urlHandler`). +- Fixed inbox HTML messages forwarding unknown `iterable://` links to `urlHandler`. `iterable://` is reserved for `iterable://delete` and `iterable://dismiss`; any other `iterable://` link now closes the message without calling a handler. Use `action://name` for custom actions (`customActionHandler`) and your app's own scheme for deep links (`urlHandler`). In inbox messages, `http(s)` links open with `Linking.openURL` and do not call `urlHandler`. - Fixed `Iterable.trackPushOpenWithCampaignId` on Android so the `appAlreadyRunning` argument is forwarded to the native SDK instead of always being sent as `false`. - `Iterable.inAppManager.showMessage` now rejects on iOS when the message id is empty or no longer in the native queue, matching Android. Previously the promise never settled. diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 549d344df..59c90e084 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -163,9 +163,9 @@ export const IterableInboxMessageDisplay = ({ const source = IterableActionSource.inApp; const context = new IterableActionContext(action, source); - // Track before routing, like native HTML in-apps: every link click sends - // click and close(link), then the message closes. Unknown iterable:// - // paths close without calling urlHandler or customActionHandler. + // Like native HTML in-apps, every link click sends click and close(link) + // and then closes the message. Unknown iterable:// paths close without + // calling urlHandler or customActionHandler. Iterable.trackInAppClick( rowViewModel.inAppMessage, IterableInAppLocation.inbox,