From 4f568ba2306aac9fa9a475f9362ae278c507220f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 12:51:40 -0700 Subject: [PATCH 01/18] feat: implement EmbeddedSessionManager --- .../EmbeddedSessionManager.test.tsx | 58 +++++++++++++++++++ .../components/EmbeddedSessionManager.tsx | 41 +++++++++++++ .../IterableEmbeddedBanner.test.tsx | 4 ++ .../IterableEmbeddedBanner.tsx | 3 + .../IterableEmbeddedCard.test.tsx | 4 ++ .../IterableEmbeddedCard.tsx | 3 + .../IterableEmbeddedNotification.test.tsx | 4 ++ .../IterableEmbeddedNotification.tsx | 3 + src/embedded/components/index.ts | 1 + .../context/EmbeddedSessionContext.ts | 6 ++ src/embedded/hooks/index.ts | 1 + .../useWarnIfOutsideEmbeddedSession.test.tsx | 48 +++++++++++++++ .../hooks/useWarnIfOutsideEmbeddedSession.ts | 19 ++++++ 13 files changed, 195 insertions(+) create mode 100644 src/embedded/components/EmbeddedSessionManager.test.tsx create mode 100644 src/embedded/components/EmbeddedSessionManager.tsx create mode 100644 src/embedded/context/EmbeddedSessionContext.ts create mode 100644 src/embedded/hooks/useWarnIfOutsideEmbeddedSession.test.tsx create mode 100644 src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts diff --git a/src/embedded/components/EmbeddedSessionManager.test.tsx b/src/embedded/components/EmbeddedSessionManager.test.tsx new file mode 100644 index 000000000..2c8cd4dbf --- /dev/null +++ b/src/embedded/components/EmbeddedSessionManager.test.tsx @@ -0,0 +1,58 @@ +import { render } from '@testing-library/react-native'; +import { Text } from 'react-native'; + +import { Iterable } from '../../core/classes/Iterable'; +import { EmbeddedSessionManager } from './EmbeddedSessionManager'; + +describe('EmbeddedSessionManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Iterable.embeddedManager, 'startSession').mockImplementation( + () => {} + ); + jest.spyOn(Iterable.embeddedManager, 'endSession').mockImplementation( + () => {} + ); + }); + + it('renders its children', () => { + const { getByTestId } = render( + + hello + + ); + + expect(getByTestId('embedded-child')).toBeTruthy(); + }); + + it('starts session on mount and ends session on unmount', () => { + const { unmount } = render( + + child + + ); + + expect(Iterable.embeddedManager.startSession).toHaveBeenCalledTimes(1); + expect(Iterable.embeddedManager.endSession).not.toHaveBeenCalled(); + + unmount(); + + expect(Iterable.embeddedManager.endSession).toHaveBeenCalledTimes(1); + }); + + it('does not double track session for nested wrappers', () => { + const { unmount } = render( + + + nested child + + + ); + + expect(Iterable.embeddedManager.startSession).toHaveBeenCalledTimes(1); + + unmount(); + + expect(Iterable.embeddedManager.endSession).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx new file mode 100644 index 000000000..eda296995 --- /dev/null +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from 'react'; +import { useContext, useEffect } from 'react'; +import { View } from 'react-native'; +import type { ViewProps } from 'react-native'; + +import { Iterable } from '../../core/classes/Iterable'; +import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; + +interface EmbeddedSessionManagerProps extends ViewProps { + children?: ReactNode; +} + +/** + * Wraps embedded content and tracks an embedded session for its lifecycle. + * + * If nested, only the top-most wrapper starts and ends the session. + */ +export const EmbeddedSessionManager = ({ + children, + ...viewProps +}: EmbeddedSessionManagerProps) => { + const hasActiveParentSession = useContext(EmbeddedSessionContext); + + useEffect(() => { + if (hasActiveParentSession) { + return; + } + + Iterable.embeddedManager.startSession(); + + return () => { + Iterable.embeddedManager.endSession(); + }; + }, [hasActiveParentSession]); + + return ( + + {children} + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx index aab2230fa..1c61c615d 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx @@ -14,6 +14,10 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); +jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ + useWarnIfOutsideEmbeddedSession: jest.fn(), +})); + const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 92d568e30..08cdb0614 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -11,6 +11,7 @@ import { import { IterableEmbeddedViewType } from '../../enums'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import { useWarnIfOutsideEmbeddedSession } from '../../hooks/useWarnIfOutsideEmbeddedSession'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { styles, @@ -28,6 +29,8 @@ export const IterableEmbeddedBanner = ({ onButtonClick, onMessageClick, }: IterableEmbeddedComponentProps) => { + useWarnIfOutsideEmbeddedSession('IterableEmbeddedBanner'); + const { parsedStyles, media, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Banner, { message, diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx index c423595e9..34ac944a6 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx @@ -20,6 +20,10 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); +jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ + useWarnIfOutsideEmbeddedSession: jest.fn(), +})); + const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 4c9148c2f..32d765fee 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -12,6 +12,7 @@ import { import { IterableLogoGrey } from '../../../core/assets'; import { IterableEmbeddedViewType } from '../../enums'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import { useWarnIfOutsideEmbeddedSession } from '../../hooks/useWarnIfOutsideEmbeddedSession'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; @@ -25,6 +26,8 @@ export const IterableEmbeddedCard = ({ onButtonClick = () => {}, onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { + useWarnIfOutsideEmbeddedSession('IterableEmbeddedCard'); + const { handleButtonClick, handleMessageClick, diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx index 3619cfc4f..55976de07 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx @@ -14,6 +14,10 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); +jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ + useWarnIfOutsideEmbeddedSession: jest.fn(), +})); + const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index 856ef4da9..d4565cd49 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -9,6 +9,7 @@ import { import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import { useWarnIfOutsideEmbeddedSession } from '../../hooks/useWarnIfOutsideEmbeddedSession'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { styles } from './IterableEmbeddedNotification.styles'; @@ -18,6 +19,8 @@ export const IterableEmbeddedNotification = ({ onButtonClick, onMessageClick, }: IterableEmbeddedComponentProps) => { + useWarnIfOutsideEmbeddedSession('IterableEmbeddedNotification'); + const { parsedStyles, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Notification, { message, diff --git a/src/embedded/components/index.ts b/src/embedded/components/index.ts index 15af78aba..174436b33 100644 --- a/src/embedded/components/index.ts +++ b/src/embedded/components/index.ts @@ -1,3 +1,4 @@ +export * from './EmbeddedSessionManager'; export * from './IterableEmbeddedBanner'; export * from './IterableEmbeddedCard'; export * from './IterableEmbeddedNotification'; diff --git a/src/embedded/context/EmbeddedSessionContext.ts b/src/embedded/context/EmbeddedSessionContext.ts new file mode 100644 index 000000000..ee6322684 --- /dev/null +++ b/src/embedded/context/EmbeddedSessionContext.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react'; + +/** + * `true` when the tree is under `EmbeddedSessionManager`. + */ +export const EmbeddedSessionContext = createContext(false); diff --git a/src/embedded/hooks/index.ts b/src/embedded/hooks/index.ts index cbca753d9..4c0859369 100644 --- a/src/embedded/hooks/index.ts +++ b/src/embedded/hooks/index.ts @@ -1 +1,2 @@ export * from './useEmbeddedView'; +export * from './useWarnIfOutsideEmbeddedSession'; diff --git a/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.test.tsx b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.test.tsx new file mode 100644 index 000000000..5e08ed4d1 --- /dev/null +++ b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.test.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from 'react'; +import { act, renderHook } from '@testing-library/react-native'; + +import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; +import { useWarnIfOutsideEmbeddedSession } from './useWarnIfOutsideEmbeddedSession'; + +describe('useWarnIfOutsideEmbeddedSession', () => { + it('logs a warning when not under EmbeddedSessionManager', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + renderHook(() => useWarnIfOutsideEmbeddedSession('TestComponent')); + + await act(async () => { + await Promise.resolve(); + }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + const firstArg = warnSpy.mock.calls[0]?.[0]; + expect(firstArg).toBeDefined(); + const message = String(firstArg); + expect(message).toContain('TestComponent'); + expect(message).toContain('EmbeddedSessionManager'); + + warnSpy.mockRestore(); + }); + + it('does not log when under EmbeddedSessionManager', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + renderHook(() => useWarnIfOutsideEmbeddedSession('TestComponent'), { + wrapper, + }); + + await act(async () => { + await Promise.resolve(); + }); + + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); +}); diff --git a/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts new file mode 100644 index 000000000..601560b00 --- /dev/null +++ b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts @@ -0,0 +1,19 @@ +import { useContext, useEffect } from 'react'; + +import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; + +/** + * Logs a dev warning when an embedded UI component is not under + * `EmbeddedSessionManager`. + */ +export function useWarnIfOutsideEmbeddedSession(componentName: string): void { + const isInsideEmbeddedSession = useContext(EmbeddedSessionContext); + + useEffect(() => { + if (!isInsideEmbeddedSession) { + console.warn( + `[Iterable] ${componentName} should be rendered inside so embedded session tracking works correctly.` + ); + } + }, [componentName, isInsideEmbeddedSession]); +} From 20a69497572e0cf6a96433074a9640151f306404 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 13:06:24 -0700 Subject: [PATCH 02/18] feat: add index file to export EmbeddedSessionContext --- src/embedded/context/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/embedded/context/index.ts diff --git a/src/embedded/context/index.ts b/src/embedded/context/index.ts new file mode 100644 index 000000000..732647039 --- /dev/null +++ b/src/embedded/context/index.ts @@ -0,0 +1 @@ +export * from './EmbeddedSessionContext'; From 2a8613122f9862c6c337ab40beaa66a9e7cf4c5c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 13:15:13 -0700 Subject: [PATCH 03/18] feat: add EmbeddedSessionDevWarning component and integrate it into existing embedded components --- .../EmbeddedSessionDevWarning.styles.ts | 23 ++++++++++++ .../EmbeddedSessionDevWarning.test.tsx | 20 +++++++++++ .../EmbeddedSessionDevWarning.tsx | 35 +++++++++++++++++++ .../IterableEmbeddedBanner.tsx | 12 ++++++- .../IterableEmbeddedCard.tsx | 12 ++++++- .../IterableEmbeddedNotification.tsx | 12 ++++++- .../useWarnIfOutsideEmbeddedSession.test.tsx | 13 ++++--- .../hooks/useWarnIfOutsideEmbeddedSession.ts | 15 +++++--- 8 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.styles.ts create mode 100644 src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.test.tsx create mode 100644 src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx diff --git a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.styles.ts b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.styles.ts new file mode 100644 index 000000000..1b175b538 --- /dev/null +++ b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.styles.ts @@ -0,0 +1,23 @@ +import { StyleSheet } from 'react-native'; + +const ERROR_BANNER_BACKGROUND = '#FEF2F2'; +const ERROR_BANNER_BORDER = '#FECACA'; +const ERROR_BANNER_TEXT = '#991B1B'; + +export const styles = StyleSheet.create({ + banner: { + alignSelf: 'stretch', + backgroundColor: ERROR_BANNER_BACKGROUND, + borderColor: ERROR_BANNER_BORDER, + borderWidth: 1, + marginBottom: 8, + paddingHorizontal: 12, + paddingVertical: 8, + }, + text: { + color: ERROR_BANNER_TEXT, + fontSize: 12, + fontWeight: '600', + lineHeight: 16, + }, +}); diff --git a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.test.tsx b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.test.tsx new file mode 100644 index 000000000..8f19bf8e0 --- /dev/null +++ b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.test.tsx @@ -0,0 +1,20 @@ +import { render } from '@testing-library/react-native'; + +import { EmbeddedSessionDevWarning } from './EmbeddedSessionDevWarning'; + +describe('EmbeddedSessionDevWarning', () => { + it('renders nothing when not visible', () => { + const { queryByRole } = render( + + ); + expect(queryByRole('alert')).toBeNull(); + }); + + it('renders warning text when visible', () => { + const { getByText } = render( + + ); + expect(getByText(/IterableEmbeddedBanner/)).toBeTruthy(); + expect(getByText(/EmbeddedSessionManager/)).toBeTruthy(); + }); +}); diff --git a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx new file mode 100644 index 000000000..7d66a0de2 --- /dev/null +++ b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx @@ -0,0 +1,35 @@ +import { Text, View } from 'react-native'; + +import { styles } from './EmbeddedSessionDevWarning.styles'; + +interface EmbeddedSessionDevWarningProps { + /** When true, shows the dev-only banner. */ + visible: boolean; + /** Name of the component (for the message). */ + componentName: string; +} + +/** + * Dev-only error-styled banner when embedded views are not wrapped in + * `EmbeddedSessionManager`. + */ +export const EmbeddedSessionDevWarning = ({ + visible, + componentName, +}: EmbeddedSessionDevWarningProps) => { + if (!visible) { + return null; + } + + const message = `[Iterable] ${componentName}: wrap this view in so embedded session tracking works correctly.`; + + return ( + + {message} + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 08cdb0614..3237594e8 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -13,12 +13,15 @@ import { IterableEmbeddedViewType } from '../../enums'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import { useWarnIfOutsideEmbeddedSession } from '../../hooks/useWarnIfOutsideEmbeddedSession'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import { EmbeddedSessionDevWarning } from '../EmbeddedSessionDevWarning/EmbeddedSessionDevWarning'; import { styles, IMAGE_HEIGHT, IMAGE_WIDTH, } from './IterableEmbeddedBanner.styles'; +const COMPONENT_NAME = 'IterableEmbeddedBanner'; + /** * TODO: figure out how default action works. */ @@ -29,7 +32,8 @@ export const IterableEmbeddedBanner = ({ onButtonClick, onMessageClick, }: IterableEmbeddedComponentProps) => { - useWarnIfOutsideEmbeddedSession('IterableEmbeddedBanner'); + const showEmbeddedSessionWarning = + useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); const { parsedStyles, media, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Banner, { @@ -41,6 +45,12 @@ export const IterableEmbeddedBanner = ({ const buttons = message.elements?.buttons ?? []; + if (showEmbeddedSessionWarning) { + return ( + + ); + } + return ( {}, onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { - useWarnIfOutsideEmbeddedSession('IterableEmbeddedCard'); + const showEmbeddedSessionWarning = + useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); const { handleButtonClick, @@ -41,6 +45,12 @@ export const IterableEmbeddedCard = ({ }); const buttons = message?.elements?.buttons ?? []; + if (showEmbeddedSessionWarning) { + return ( + + ); + } + return ( handleMessageClick()}> { - useWarnIfOutsideEmbeddedSession('IterableEmbeddedNotification'); + const showEmbeddedSessionWarning = + useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); const { parsedStyles, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Notification, { @@ -31,6 +35,12 @@ export const IterableEmbeddedNotification = ({ const buttons = message.elements?.buttons ?? []; + if (showEmbeddedSessionWarning) { + return ( + + ); + } + return ( handleMessageClick()}> { it('logs a warning when not under EmbeddedSessionManager', async () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - renderHook(() => useWarnIfOutsideEmbeddedSession('TestComponent')); + const { result } = renderHook(() => + useWarnIfOutsideEmbeddedSession('TestComponent') + ); await act(async () => { await Promise.resolve(); }); + expect(result.current).toBe(true); expect(warnSpy).toHaveBeenCalledTimes(1); const firstArg = warnSpy.mock.calls[0]?.[0]; expect(firstArg).toBeDefined(); @@ -33,14 +36,16 @@ describe('useWarnIfOutsideEmbeddedSession', () => { ); - renderHook(() => useWarnIfOutsideEmbeddedSession('TestComponent'), { - wrapper, - }); + const { result } = renderHook( + () => useWarnIfOutsideEmbeddedSession('TestComponent'), + { wrapper } + ); await act(async () => { await Promise.resolve(); }); + expect(result.current).toBe(false); expect(warnSpy).not.toHaveBeenCalled(); warnSpy.mockRestore(); diff --git a/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts index 601560b00..09df7eff1 100644 --- a/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts +++ b/src/embedded/hooks/useWarnIfOutsideEmbeddedSession.ts @@ -3,17 +3,22 @@ import { useContext, useEffect } from 'react'; import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; /** - * Logs a dev warning when an embedded UI component is not under - * `EmbeddedSessionManager`. + * When the embedded UI component is not under `EmbeddedSessionManager`, logs + * `console.warn` and returns `true` so callers can show on-screen dev text. */ -export function useWarnIfOutsideEmbeddedSession(componentName: string): void { +export function useWarnIfOutsideEmbeddedSession( + componentName: string +): boolean { const isInsideEmbeddedSession = useContext(EmbeddedSessionContext); + const isOutsideEmbeddedSession = !isInsideEmbeddedSession; useEffect(() => { - if (!isInsideEmbeddedSession) { + if (isOutsideEmbeddedSession) { console.warn( `[Iterable] ${componentName} should be rendered inside so embedded session tracking works correctly.` ); } - }, [componentName, isInsideEmbeddedSession]); + }, [componentName, isOutsideEmbeddedSession]); + + return isOutsideEmbeddedSession; } From e5c4dbdb73188dfb8d4a9a36abe35249360e0b9b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 13:23:58 -0700 Subject: [PATCH 04/18] feat: integrate EmbeddedSessionManager into Embedded component and update exports --- example/src/components/Embedded/Embedded.tsx | 19 +++++++++++-------- .../components/EmbeddedSessionManager.tsx | 1 + src/index.tsx | 5 +++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 189293d37..f56180820 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -14,6 +14,7 @@ import { type IterableEmbeddedViewConfig, IterableEmbeddedView, IterableEmbeddedViewType, + EmbeddedSessionManager, } from '@iterable/react-native-sdk'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -230,14 +231,16 @@ export const Embedded = () => { - {embeddedMessages.map((message) => ( - - ))} + + {embeddedMessages.map((message) => ( + + ))} + diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx index eda296995..316b1717c 100644 --- a/src/embedded/components/EmbeddedSessionManager.tsx +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -26,6 +26,7 @@ export const EmbeddedSessionManager = ({ return; } + Iterable.embeddedManager.syncMessages(); Iterable.embeddedManager.startSession(); return () => { diff --git a/src/index.tsx b/src/index.tsx index b4ba8f5cf..68143d66e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,9 +33,14 @@ export type { IterableRetryPolicy, } from './core/types'; export { + EmbeddedSessionManager, + IterableEmbeddedBanner, + IterableEmbeddedCard, IterableEmbeddedManager, + IterableEmbeddedNotification, IterableEmbeddedView, IterableEmbeddedViewType, + useEmbeddedView, type IterableEmbeddedComponentProps, type IterableEmbeddedMessage, type IterableEmbeddedMessageElements, From 57c383b941c1927b9f67befebb58c9164f828212 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 13:40:14 -0700 Subject: [PATCH 05/18] refactor: simplify Embedded component by removing unused session management functions --- .../components/Embedded/Embedded.styles.ts | 6 +-- example/src/components/Embedded/Embedded.tsx | 51 ++++++------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index a1fb26b7e..396b651f4 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -21,10 +21,8 @@ const styles = StyleSheet.create({ buttonText, container, embeddedSection: { - display: 'flex', - flexDirection: 'column', - gap: 16, - paddingHorizontal: 16, + + marginBottom: 16, }, hr, inputContainer: { diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index f56180820..e1a2a4f28 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,3 +1,4 @@ +import { useCallback, useState } from 'react'; import { Alert, Modal, @@ -7,16 +8,16 @@ import { TouchableOpacity, View, } from 'react-native'; -import { useCallback, useState } from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; + import { + EmbeddedSessionManager, Iterable, - type IterableEmbeddedMessage, - type IterableEmbeddedViewConfig, IterableEmbeddedView, IterableEmbeddedViewType, - EmbeddedSessionManager, + type IterableEmbeddedMessage, + type IterableEmbeddedViewConfig, } from '@iterable/react-native-sdk'; -import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './Embedded.styles'; @@ -45,18 +46,6 @@ export const Embedded = () => { const idsToFetch = parsedPlacementIds.length > 0 ? parsedPlacementIds : null; - const syncEmbeddedMessages = useCallback(() => { - Iterable.embeddedManager.syncMessages(); - }, []); - - const startEmbeddedSession = useCallback(() => { - Iterable.embeddedManager.startSession(); - }, []); - - const endEmbeddedSession = useCallback(() => { - Iterable.embeddedManager.endSession(); - }, []); - const getEmbeddedMessages = useCallback(() => { Iterable.embeddedManager .getMessages(idsToFetch) @@ -98,9 +87,6 @@ export const Embedded = () => { )} - - Enter placement IDs to fetch embedded messages - Select View Type: @@ -165,15 +151,6 @@ export const Embedded = () => { - - Sync messages - - - Start session - - - End session - Set view config @@ -230,18 +207,20 @@ export const Embedded = () => { - - - {embeddedMessages.map((message) => ( + + {embeddedMessages.map((message) => ( + - ))} - - + + ))} + ); From a522b4964ec770f126c9a2f01462195522909f48 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 13:47:13 -0700 Subject: [PATCH 06/18] fix: restore syncMessages call in EmbeddedSessionManager for proper session management --- src/core/classes/Iterable.ts | 2 ++ src/embedded/components/EmbeddedSessionManager.tsx | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 983fab49b..3a5cc0172 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1108,6 +1108,8 @@ export class Iterable { } ); } + + Iterable.embeddedManager.syncMessages(); } } diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx index 316b1717c..eda296995 100644 --- a/src/embedded/components/EmbeddedSessionManager.tsx +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -26,7 +26,6 @@ export const EmbeddedSessionManager = ({ return; } - Iterable.embeddedManager.syncMessages(); Iterable.embeddedManager.startSession(); return () => { From 324b7abe2494acf2fb887b15d4aa466439f6d745 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 14:02:26 -0700 Subject: [PATCH 07/18] style: rename embeddedSection to embeddedItem and update button styles in Embedded component --- .../components/Embedded/Embedded.styles.ts | 3 +- example/src/components/Embedded/Embedded.tsx | 238 +++++++++--------- 2 files changed, 123 insertions(+), 118 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 396b651f4..618eec0a1 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -20,8 +20,7 @@ const styles = StyleSheet.create({ button, buttonText, container, - embeddedSection: { - + embeddedItem: { marginBottom: 16, }, hr, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index e1a2a4f28..ea379a178 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -77,140 +77,146 @@ export const Embedded = () => { }, []); return ( - - Embedded - {!Iterable.embeddedManager.isEnabled && ( - - - ⚠️ Embedded messaging is disabled. Please enable it in your Iterable - config. - - - )} - - - Select View Type: - - - setSelectedViewType(IterableEmbeddedViewType.Banner) - } - > - + + Embedded + {!Iterable.embeddedManager.isEnabled && ( + + + ⚠️ Embedded messaging is disabled. Please enable it in your + Iterable config. + + + )} + + + Select View Type: + + + setSelectedViewType(IterableEmbeddedViewType.Banner) + } > - Banner - - - setSelectedViewType(IterableEmbeddedViewType.Card)} - > - + Banner + + + + setSelectedViewType(IterableEmbeddedViewType.Card) + } > - Card - - - - setSelectedViewType(IterableEmbeddedViewType.Notification) - } - > - + Card + + + + setSelectedViewType(IterableEmbeddedViewType.Notification) + } > - Notification - - + + Notification + + + - - - Set view config - - - Placement IDs (comma-separated): - - - - Get messages for placement ids - + + Set view config - - - - - + + Placement IDs (comma-separated): - - - Cancel - - - Apply - - + + + Get messages for placement ids + + - - - - + + + + + + + Cancel + + + Apply + + + + + + + {embeddedMessages.map((message) => ( { /> ))} - - - + + + ); }; From 65c19b892a1d86b33058960f70cd6bd6271b28fd Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 14:27:31 -0700 Subject: [PATCH 08/18] feat: enhance EmbeddedSessionManager to manage session state based on screen focus --- example/src/components/Embedded/Embedded.tsx | 4 +- .../EmbeddedSessionManager.test.tsx | 40 +++++++++++++++++++ .../components/EmbeddedSessionManager.tsx | 17 ++++++-- src/index.tsx | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index ea379a178..74fb4b0ab 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,3 +1,4 @@ +import { useIsFocused } from '@react-navigation/native'; import { useCallback, useState } from 'react'; import { Alert, @@ -25,6 +26,7 @@ const DEFAULT_CONFIG_JSON = `{ }`; export const Embedded = () => { + const isFocused = useIsFocused(); const [placementIdsInput, setPlacementIdsInput] = useState(''); const [embeddedMessages, setEmbeddedMessages] = useState< IterableEmbeddedMessage[] @@ -77,7 +79,7 @@ export const Embedded = () => { }, []); return ( - + Embedded {!Iterable.embeddedManager.isEnabled && ( diff --git a/src/embedded/components/EmbeddedSessionManager.test.tsx b/src/embedded/components/EmbeddedSessionManager.test.tsx index 2c8cd4dbf..c47161e13 100644 --- a/src/embedded/components/EmbeddedSessionManager.test.tsx +++ b/src/embedded/components/EmbeddedSessionManager.test.tsx @@ -55,4 +55,44 @@ describe('EmbeddedSessionManager', () => { expect(Iterable.embeddedManager.endSession).toHaveBeenCalledTimes(1); }); + + it('does not start session when isActive is false', () => { + const { unmount } = render( + + child + + ); + + expect(Iterable.embeddedManager.startSession).not.toHaveBeenCalled(); + + unmount(); + + expect(Iterable.embeddedManager.endSession).not.toHaveBeenCalled(); + }); + + it('ends session when isActive becomes false and restarts when true again', () => { + const { rerender } = render( + + child + + ); + + expect(Iterable.embeddedManager.startSession).toHaveBeenCalledTimes(1); + + rerender( + + child + + ); + + expect(Iterable.embeddedManager.endSession).toHaveBeenCalledTimes(1); + + rerender( + + child + + ); + + expect(Iterable.embeddedManager.startSession).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx index eda296995..4f213ffc5 100644 --- a/src/embedded/components/EmbeddedSessionManager.tsx +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -6,8 +6,18 @@ import type { ViewProps } from 'react-native'; import { Iterable } from '../../core/classes/Iterable'; import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; -interface EmbeddedSessionManagerProps extends ViewProps { +export interface EmbeddedSessionManagerProps extends ViewProps { children?: ReactNode; + /** + * Is the current screen in focus? + * + * When `false`, this wrapper does not start an embedded session (e.g. host + * screen not focused). Defaults to `true`. + * + * This is not necessary to use. It is only useful if you want to avoid + * starting the session when the screen is hidden but still technically there. + */ + isActive?: boolean; } /** @@ -17,12 +27,13 @@ interface EmbeddedSessionManagerProps extends ViewProps { */ export const EmbeddedSessionManager = ({ children, + isActive = true, ...viewProps }: EmbeddedSessionManagerProps) => { const hasActiveParentSession = useContext(EmbeddedSessionContext); useEffect(() => { - if (hasActiveParentSession) { + if (hasActiveParentSession || !isActive) { return; } @@ -31,7 +42,7 @@ export const EmbeddedSessionManager = ({ return () => { Iterable.embeddedManager.endSession(); }; - }, [hasActiveParentSession]); + }, [hasActiveParentSession, isActive]); return ( diff --git a/src/index.tsx b/src/index.tsx index 68143d66e..d3fb03e16 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -41,6 +41,7 @@ export { IterableEmbeddedView, IterableEmbeddedViewType, useEmbeddedView, + type EmbeddedSessionManagerProps, type IterableEmbeddedComponentProps, type IterableEmbeddedMessage, type IterableEmbeddedMessageElements, From 394930cd6662734f67e53fba3a8f330590208e38 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 14:34:12 -0700 Subject: [PATCH 09/18] refactor: streamline Embedded component layout and improve button styling for view type selection --- example/src/components/Embedded/Embedded.tsx | 237 +++++++++---------- 1 file changed, 114 insertions(+), 123 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 74fb4b0ab..e266209ab 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -79,148 +79,139 @@ export const Embedded = () => { }, []); return ( - - - Embedded - {!Iterable.embeddedManager.isEnabled && ( - - - ⚠️ Embedded messaging is disabled. Please enable it in your - Iterable config. - - - )} - - - Select View Type: - - + Embedded + {!Iterable.embeddedManager.isEnabled && ( + + + ⚠️ Embedded messaging is disabled. Please enable it in your Iterable + config. + + + )} + + + Select View Type: + + + setSelectedViewType(IterableEmbeddedViewType.Banner) + } + > + - setSelectedViewType(IterableEmbeddedViewType.Banner) - } > - - Banner - - - + + setSelectedViewType(IterableEmbeddedViewType.Card)} + > + - setSelectedViewType(IterableEmbeddedViewType.Card) - } > - - Card - - - + + + setSelectedViewType(IterableEmbeddedViewType.Notification) + } + > + - setSelectedViewType(IterableEmbeddedViewType.Notification) - } > - - Notification - - - + Notification + + - - Set view config + + + Set view config + + + Placement IDs (comma-separated): + + + + Get messages for placement ids + - - Placement IDs (comma-separated): + + + + + - - - Get messages for placement ids - - - - - - - - - - - Cancel - - - Apply - - + + + Cancel + + + Apply + - - + + + + {embeddedMessages.map((message) => ( - + { ))} - - + + ); }; From 832ec46222115750d6cbd5ce1d500fb722c235b4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 14:37:44 -0700 Subject: [PATCH 10/18] refactor: update EmbeddedSessionManager to use PropsWithChildren and simplify props handling --- src/embedded/components/EmbeddedSessionManager.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx index 4f213ffc5..9cd6987cd 100644 --- a/src/embedded/components/EmbeddedSessionManager.tsx +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -1,12 +1,10 @@ -import type { ReactNode } from 'react'; +import type { PropsWithChildren, ReactNode } from 'react'; import { useContext, useEffect } from 'react'; -import { View } from 'react-native'; -import type { ViewProps } from 'react-native'; import { Iterable } from '../../core/classes/Iterable'; import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; -export interface EmbeddedSessionManagerProps extends ViewProps { +export interface EmbeddedSessionManagerProps { children?: ReactNode; /** * Is the current screen in focus? @@ -28,8 +26,7 @@ export interface EmbeddedSessionManagerProps extends ViewProps { export const EmbeddedSessionManager = ({ children, isActive = true, - ...viewProps -}: EmbeddedSessionManagerProps) => { +}: PropsWithChildren) => { const hasActiveParentSession = useContext(EmbeddedSessionContext); useEffect(() => { @@ -46,7 +43,7 @@ export const EmbeddedSessionManager = ({ return ( - {children} + {children} ); }; From 386d193185088818840877e0fd01a054de80e802 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 14:49:30 -0700 Subject: [PATCH 11/18] refactor: remove unused exports from index and simplify EmbeddedSessionDevWarning documentation --- .../EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx | 3 +-- src/index.tsx | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx index 7d66a0de2..d1caab3fb 100644 --- a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx +++ b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx @@ -10,8 +10,7 @@ interface EmbeddedSessionDevWarningProps { } /** - * Dev-only error-styled banner when embedded views are not wrapped in - * `EmbeddedSessionManager`. + * Banner when embedded views are not wrapped in EmbeddedSessionManager`. */ export const EmbeddedSessionDevWarning = ({ visible, diff --git a/src/index.tsx b/src/index.tsx index d3fb03e16..ee622a0c2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -34,10 +34,7 @@ export type { } from './core/types'; export { EmbeddedSessionManager, - IterableEmbeddedBanner, - IterableEmbeddedCard, IterableEmbeddedManager, - IterableEmbeddedNotification, IterableEmbeddedView, IterableEmbeddedViewType, useEmbeddedView, From 249173f9b84637e4222c824cd885307d1d500776 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:02:45 -0700 Subject: [PATCH 12/18] refactor: implement session warning in IterableEmbeddedView --- .../EmbeddedSessionDevWarning.tsx | 2 +- .../EmbeddedSessionDevWarning/index.ts | 2 ++ .../IterableEmbeddedBanner.tsx | 19 ++--------- .../IterableEmbeddedCard.tsx | 34 +++++-------------- .../IterableEmbeddedNotification.tsx | 15 +------- .../components/IterableEmbeddedView.tsx | 13 +++++++ 6 files changed, 28 insertions(+), 57 deletions(-) create mode 100644 src/embedded/components/EmbeddedSessionDevWarning/index.ts diff --git a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx index d1caab3fb..756e3edab 100644 --- a/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx +++ b/src/embedded/components/EmbeddedSessionDevWarning/EmbeddedSessionDevWarning.tsx @@ -20,7 +20,7 @@ export const EmbeddedSessionDevWarning = ({ return null; } - const message = `[Iterable] ${componentName}: wrap this view in so embedded session tracking works correctly.`; + const message = `[Iterable] ${componentName}: wrap this component in so embedded session tracking works correctly.`; return ( { - const showEmbeddedSessionWarning = - useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); - const { parsedStyles, media, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Banner, { message, @@ -45,12 +38,6 @@ export const IterableEmbeddedBanner = ({ const buttons = message.elements?.buttons ?? []; - if (showEmbeddedSessionWarning) { - return ( - - ); - } - return ( {}, onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { - const showEmbeddedSessionWarning = - useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); - - const { - handleButtonClick, - handleMessageClick, - media, - parsedStyles, - } = useEmbeddedView(IterableEmbeddedViewType.Card, { - message, - config, - onButtonClick, - onMessageClick, - }); + const { handleButtonClick, handleMessageClick, media, parsedStyles } = + useEmbeddedView(IterableEmbeddedViewType.Card, { + message, + config, + onButtonClick, + onMessageClick, + }); const buttons = message?.elements?.buttons ?? []; - if (showEmbeddedSessionWarning) { - return ( - - ); - } - return ( handleMessageClick()}> { - const showEmbeddedSessionWarning = - useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); - const { parsedStyles, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Notification, { message, @@ -35,12 +28,6 @@ export const IterableEmbeddedNotification = ({ const buttons = message.elements?.buttons ?? []; - if (showEmbeddedSessionWarning) { - return ( - - ); - } - return ( handleMessageClick()}> { + const showEmbeddedSessionWarning = + useWarnIfOutsideEmbeddedSession(COMPONENT_NAME); + const Cmp = useMemo(() => { switch (viewType) { case IterableEmbeddedViewType.Card: @@ -42,5 +49,11 @@ export const IterableEmbeddedView = ({ } }, [viewType]); + if (showEmbeddedSessionWarning) { + return ( + + ); + } + return Cmp ? : null; }; From 8d9d640ba3ccbcd21eddeaa11f25a99868dc116b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:06:07 -0700 Subject: [PATCH 13/18] test: refactor IterableEmbeddedView tests to use renderWithEmbeddedSession for context management --- .../components/IterableEmbeddedView.test.tsx | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedView.test.tsx b/src/embedded/components/IterableEmbeddedView.test.tsx index 4bcc47dcf..49f610869 100644 --- a/src/embedded/components/IterableEmbeddedView.test.tsx +++ b/src/embedded/components/IterableEmbeddedView.test.tsx @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ReactElement } from 'react'; import { render } from '@testing-library/react-native'; +import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; import { IterableEmbeddedView } from './IterableEmbeddedView'; import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; @@ -20,6 +22,12 @@ jest.mock('./IterableEmbeddedNotification', () => ({ IterableEmbeddedNotification: jest.fn(() => null), })); +function renderWithEmbeddedSession(ui: ReactElement) { + return render( + {ui} + ); +} + describe('IterableEmbeddedView', () => { const mockMessage = { metadata: { @@ -46,7 +54,7 @@ describe('IterableEmbeddedView', () => { describe('View Type Rendering', () => { it('should render IterableEmbeddedCard when viewType is Card', () => { - render( + renderWithEmbeddedSession( { }); it('should render IterableEmbeddedNotification when viewType is Notification', () => { - render( + renderWithEmbeddedSession( { }); it('should render IterableEmbeddedBanner when viewType is Banner', () => { - render( + renderWithEmbeddedSession( { }); it('should render null for invalid viewType', () => { - render( + renderWithEmbeddedSession( { }); it('should render null for undefined viewType', () => { - render( + renderWithEmbeddedSession( { describe('Props Passing', () => { it('should pass message prop to Card component', () => { - render( + renderWithEmbeddedSession( { }); it('should pass message prop to Banner component', () => { - render( + renderWithEmbeddedSession( { }); it('should pass message prop to Notification component', () => { - render( + renderWithEmbeddedSession( { }); it('should pass config prop to child component', () => { - render( + renderWithEmbeddedSession( { }); it('should pass onButtonClick prop to child component', () => { - render( + renderWithEmbeddedSession( { }); it('should pass all props to child component', () => { - render( + renderWithEmbeddedSession( { describe('Component Memoization', () => { it('should memoize component selection based on viewType', () => { - const { rerender } = render( + const { rerender } = renderWithEmbeddedSession( { }; rerender( - + + + ); // Should still render Card component (memoization means same component reference) @@ -243,7 +253,7 @@ describe('IterableEmbeddedView', () => { }); it('should update component when viewType changes', () => { - const { rerender } = render( + const { rerender } = renderWithEmbeddedSession( { // Re-render with different viewType rerender( - + + + ); expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); @@ -269,7 +281,7 @@ describe('IterableEmbeddedView', () => { describe('Edge Cases', () => { it('should handle null config gracefully', () => { - render( + renderWithEmbeddedSession( { }); it('should handle undefined config gracefully', () => { - render( + renderWithEmbeddedSession( { }); it('should handle missing onButtonClick gracefully', () => { - render( + renderWithEmbeddedSession( { it('should handle numeric viewType values correctly', () => { // Test with numeric value 0 (Banner) - render(); + renderWithEmbeddedSession(); expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); jest.clearAllMocks(); // Test with numeric value 1 (Card) - render(); + renderWithEmbeddedSession(); expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); jest.clearAllMocks(); // Test with numeric value 2 (Notification) - render(); + renderWithEmbeddedSession(); expect(IterableEmbeddedNotification).toHaveBeenCalledTimes(1); }); }); @@ -337,7 +349,7 @@ describe('IterableEmbeddedView', () => { describe('Component Type Verification', () => { it('should render correct component type for each enum value', () => { // Verify Banner enum value - const bannerResult = render( + const bannerResult = renderWithEmbeddedSession( { jest.clearAllMocks(); // Verify Card enum value - const cardResult = render( + const cardResult = renderWithEmbeddedSession( { jest.clearAllMocks(); // Verify Notification enum value - render( + renderWithEmbeddedSession( Date: Thu, 19 Mar 2026 15:07:35 -0700 Subject: [PATCH 14/18] refactor: remove unused session warning mocks from IterableEmbedded tests --- .../IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx | 4 ---- .../IterableEmbeddedCard/IterableEmbeddedCard.test.tsx | 4 ---- .../IterableEmbeddedNotification.test.tsx | 4 ---- 3 files changed, 12 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx index 1c61c615d..aab2230fa 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx @@ -14,10 +14,6 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); -jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ - useWarnIfOutsideEmbeddedSession: jest.fn(), -})); - const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx index 34ac944a6..c423595e9 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx @@ -20,10 +20,6 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); -jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ - useWarnIfOutsideEmbeddedSession: jest.fn(), -})); - const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx index 55976de07..3619cfc4f 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx @@ -14,10 +14,6 @@ jest.mock('../../hooks/useEmbeddedView', () => ({ useEmbeddedView: jest.fn(), })); -jest.mock('../../hooks/useWarnIfOutsideEmbeddedSession', () => ({ - useWarnIfOutsideEmbeddedSession: jest.fn(), -})); - const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< typeof useEmbeddedView >; From 102669c6b328a93f8de46442c4bb6d12ba7a6753 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:12:15 -0700 Subject: [PATCH 15/18] refactor: update Embedded component layout to use embeddedSection for improved styling and structure --- .../src/components/Embedded/Embedded.styles.ts | 7 +++++-- example/src/components/Embedded/Embedded.tsx | 17 +++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 618eec0a1..a1fb26b7e 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -20,8 +20,11 @@ const styles = StyleSheet.create({ button, buttonText, container, - embeddedItem: { - marginBottom: 16, + embeddedSection: { + display: 'flex', + flexDirection: 'column', + gap: 16, + paddingHorizontal: 16, }, hr, inputContainer: { diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index e266209ab..7af8bfc18 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -208,19 +208,20 @@ export const Embedded = () => { - - - {embeddedMessages.map((message) => ( - + + + + {embeddedMessages.map((message) => ( - - ))} - - + ))} + + + ); }; From 090a83b4bae8c570b194556967f5d7f8b26862d0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:14:18 -0700 Subject: [PATCH 16/18] refactor: clarify documentation in EmbeddedSessionManager regarding screen focus and usage --- src/embedded/components/EmbeddedSessionManager.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embedded/components/EmbeddedSessionManager.tsx b/src/embedded/components/EmbeddedSessionManager.tsx index 9cd6987cd..6c9fe280d 100644 --- a/src/embedded/components/EmbeddedSessionManager.tsx +++ b/src/embedded/components/EmbeddedSessionManager.tsx @@ -7,8 +7,6 @@ import { EmbeddedSessionContext } from '../context/EmbeddedSessionContext'; export interface EmbeddedSessionManagerProps { children?: ReactNode; /** - * Is the current screen in focus? - * * When `false`, this wrapper does not start an embedded session (e.g. host * screen not focused). Defaults to `true`. * @@ -22,6 +20,8 @@ export interface EmbeddedSessionManagerProps { * Wraps embedded content and tracks an embedded session for its lifecycle. * * If nested, only the top-most wrapper starts and ends the session. + * + * There should only be one EmbeddedSessionManager per screen. */ export const EmbeddedSessionManager = ({ children, From 954a69fa51fc5e4b50e7a7c01cbe945dcf8fc543 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:17:11 -0700 Subject: [PATCH 17/18] refactor: improve code readability in IterableEmbeddedCard by restructuring variable declarations --- .../IterableEmbeddedBanner.tsx | 6 ++--- .../IterableEmbeddedCard.tsx | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index c69b46328..92d568e30 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -1,21 +1,21 @@ import { Image, - PixelRatio, - Pressable, Text, TouchableOpacity, View, type TextStyle, type ViewStyle, + PixelRatio, + Pressable, } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums'; import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { + styles, IMAGE_HEIGHT, IMAGE_WIDTH, - styles, } from './IterableEmbeddedBanner.styles'; /** diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 4b1ad3c8c..05b27a1e3 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,12 +1,12 @@ import { Image, PixelRatio, - Pressable, Text, TouchableOpacity, View, type TextStyle, type ViewStyle, + Pressable, } from 'react-native'; import { IterableLogoGrey } from '../../../core/assets'; @@ -25,13 +25,17 @@ export const IterableEmbeddedCard = ({ onButtonClick = () => {}, onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { - const { handleButtonClick, handleMessageClick, media, parsedStyles } = - useEmbeddedView(IterableEmbeddedViewType.Card, { - message, - config, - onButtonClick, - onMessageClick, - }); + const { + handleButtonClick, + handleMessageClick, + media, + parsedStyles, + } = useEmbeddedView(IterableEmbeddedViewType.Card, { + message, + config, + onButtonClick, + onMessageClick, + }); const buttons = message?.elements?.buttons ?? []; return ( @@ -60,7 +64,8 @@ export const IterableEmbeddedCard = ({ uri: media.url as string, height: PixelRatio.getPixelSizeForLayoutSize(IMAGE_HEIGHT), } - : IterableLogoGrey + : + IterableLogoGrey } style={ media.shouldShow From 78f957b3a2f9ed01b6b1d25997c0d5b6c8efd38d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 19 Mar 2026 15:18:30 -0700 Subject: [PATCH 18/18] refactor: reorganize imports --- .../components/IterableEmbeddedCard/IterableEmbeddedCard.tsx | 2 +- .../IterableEmbeddedNotification.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 05b27a1e3..4c9148c2f 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,12 +1,12 @@ import { Image, PixelRatio, + Pressable, Text, TouchableOpacity, View, type TextStyle, type ViewStyle, - Pressable, } from 'react-native'; import { IterableLogoGrey } from '../../../core/assets'; diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index f5098a2b6..856ef4da9 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -1,10 +1,10 @@ import { - Pressable, Text, TouchableOpacity, View, type TextStyle, type ViewStyle, + Pressable, } from 'react-native'; import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType';