From b9fbb068541992e5bb8d8e993ba7a6cac7b23732 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 16:38:18 -0700 Subject: [PATCH 01/48] feat: add IterableEmbedded components and view type enumeration for embedded messaging --- .../components/IterableEmbeddedBanner.tsx | 19 ++++++++++ .../components/IterableEmbeddedCard.tsx | 18 ++++++++++ .../IterableEmbeddedNotification.tsx | 22 ++++++++++++ .../components/IterableEmbeddedView.tsx | 36 +++++++++++++++++++ src/embedded/components/index.ts | 4 +++ .../enums/IterableEmbeddedViewType.ts | 11 ++++++ src/embedded/enums/index.ts | 1 + .../types/IterableEmbeddedComponentProps.ts | 9 +++++ .../types/IterableEmbeddedViewConfig.ts | 27 ++++++++++++++ src/embedded/types/index.ts | 2 ++ 10 files changed, 149 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedBanner.tsx create mode 100644 src/embedded/components/IterableEmbeddedCard.tsx create mode 100644 src/embedded/components/IterableEmbeddedNotification.tsx create mode 100644 src/embedded/components/IterableEmbeddedView.tsx create mode 100644 src/embedded/components/index.ts create mode 100644 src/embedded/enums/IterableEmbeddedViewType.ts create mode 100644 src/embedded/enums/index.ts create mode 100644 src/embedded/types/IterableEmbeddedComponentProps.ts create mode 100644 src/embedded/types/IterableEmbeddedViewConfig.ts diff --git a/src/embedded/components/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner.tsx new file mode 100644 index 000000000..56b4ca32b --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner.tsx @@ -0,0 +1,19 @@ +import { View, Text } from 'react-native'; + +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; + +export const IterableEmbeddedBanner = ({ + config, + message, + onButtonClick = () => {}, +}: IterableEmbeddedComponentProps) => { + console.log(`🚀 > IterableEmbeddedBanner > config:`, config); + console.log(`🚀 > IterableEmbeddedBanner > message:`, message); + console.log(`🚀 > IterableEmbeddedBanner > onButtonClick:`, onButtonClick); + + return ( + + IterableEmbeddedBanner + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard.tsx new file mode 100644 index 000000000..87b2d1940 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard.tsx @@ -0,0 +1,18 @@ +import { View, Text } from 'react-native'; +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; + +export const IterableEmbeddedCard = ({ + config, + message, + onButtonClick = () => {}, +}: IterableEmbeddedComponentProps) => { + console.log(`🚀 > IterableEmbeddedCard > config:`, config); + console.log(`🚀 > IterableEmbeddedCard > message:`, message); + console.log(`🚀 > IterableEmbeddedCard > onButtonClick:`, onButtonClick); + + return ( + + IterableEmbeddedCard + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification.tsx new file mode 100644 index 000000000..686ea01e6 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification.tsx @@ -0,0 +1,22 @@ +import { View, Text } from 'react-native'; + +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; + +export const IterableEmbeddedNotification = ({ + config, + message, + onButtonClick = () => {}, +}: IterableEmbeddedComponentProps) => { + console.log(`🚀 > IterableEmbeddedNotification > config:`, config); + console.log(`🚀 > IterableEmbeddedNotification > message:`, message); + console.log( + `🚀 > IterableEmbeddedNotification > onButtonClick:`, + onButtonClick + ); + + return ( + + IterableEmbeddedNotification + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx new file mode 100644 index 000000000..fa76f584f --- /dev/null +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; + +import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; + +import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; +import { IterableEmbeddedCard } from './IterableEmbeddedCard'; +import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; + +/** + * The props for the IterableEmbeddedView component. + */ +interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { + /** The type of view to render. */ + viewType: IterableEmbeddedViewType; +} + +export const IterableEmbeddedView = ({ + viewType, + ...props +}: IterableEmbeddedViewProps) => { + const Cmp = useMemo(() => { + switch (viewType) { + case IterableEmbeddedViewType.Card: + return IterableEmbeddedCard; + case IterableEmbeddedViewType.Notification: + return IterableEmbeddedNotification; + case IterableEmbeddedViewType.Banner: + return IterableEmbeddedBanner; + default: + return null; + } + }, [viewType]); + + return Cmp ? : null; +}; diff --git a/src/embedded/components/index.ts b/src/embedded/components/index.ts new file mode 100644 index 000000000..15af78aba --- /dev/null +++ b/src/embedded/components/index.ts @@ -0,0 +1,4 @@ +export * from './IterableEmbeddedBanner'; +export * from './IterableEmbeddedCard'; +export * from './IterableEmbeddedNotification'; +export * from './IterableEmbeddedView'; diff --git a/src/embedded/enums/IterableEmbeddedViewType.ts b/src/embedded/enums/IterableEmbeddedViewType.ts new file mode 100644 index 000000000..90a0b5d7e --- /dev/null +++ b/src/embedded/enums/IterableEmbeddedViewType.ts @@ -0,0 +1,11 @@ +/** + * The view type for an embedded message. + */ +export enum IterableEmbeddedViewType { + /** The embedded view is a banner */ + Banner = 0, + /** The embedded view is a card */ + Card = 1, + /** The embedded view is a notification */ + Notification = 2, +} diff --git a/src/embedded/enums/index.ts b/src/embedded/enums/index.ts new file mode 100644 index 000000000..511ad021b --- /dev/null +++ b/src/embedded/enums/index.ts @@ -0,0 +1 @@ +export * from './IterableEmbeddedViewType'; diff --git a/src/embedded/types/IterableEmbeddedComponentProps.ts b/src/embedded/types/IterableEmbeddedComponentProps.ts new file mode 100644 index 000000000..9f2b17670 --- /dev/null +++ b/src/embedded/types/IterableEmbeddedComponentProps.ts @@ -0,0 +1,9 @@ +import type { IterableEmbeddedMessage } from './IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import type { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; + +export interface IterableEmbeddedComponentProps { + message: IterableEmbeddedMessage; + config?: IterableEmbeddedViewConfig | null; + onButtonClick?: (button: IterableEmbeddedMessageElementsButton) => void; +} diff --git a/src/embedded/types/IterableEmbeddedViewConfig.ts b/src/embedded/types/IterableEmbeddedViewConfig.ts new file mode 100644 index 000000000..6a41edd8a --- /dev/null +++ b/src/embedded/types/IterableEmbeddedViewConfig.ts @@ -0,0 +1,27 @@ +import type { ColorValue } from 'react-native'; + +/** + * Represents view-level styling configuration for an embedded view. + */ +export interface IterableEmbeddedViewConfig { + /** Background color hex (e.g., 0xFF0000) */ + backgroundColor?: ColorValue; + /** Border color hex */ + borderColor?: ColorValue; + /** Border width in pixels */ + borderWidth?: number; + /** Corner radius in points */ + borderCornerRadius?: number; + /** Primary button background color hex */ + primaryBtnBackgroundColor?: ColorValue; + /** Primary button text color hex */ + primaryBtnTextColor?: ColorValue; + /** Secondary button background color hex */ + secondaryBtnBackgroundColor?: ColorValue; + /** Secondary button text color hex */ + secondaryBtnTextColor?: ColorValue; + /** Title text color hex */ + titleTextColor?: ColorValue; + /** Body text color hex */ + bodyTextColor?: ColorValue; +} diff --git a/src/embedded/types/index.ts b/src/embedded/types/index.ts index 29b809ebf..66deee21e 100644 --- a/src/embedded/types/index.ts +++ b/src/embedded/types/index.ts @@ -1,5 +1,7 @@ +export * from './IterableEmbeddedComponentProps'; export * from './IterableEmbeddedMessage'; export * from './IterableEmbeddedMessageElements'; export * from './IterableEmbeddedMessageElementsButton'; export * from './IterableEmbeddedMessageElementsText'; export * from './IterableEmbeddedMessageMetadata'; +export * from './IterableEmbeddedViewConfig'; From 3b252d1560da8df6ba3c6c9c735c46e6ebb96d9c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 16:39:44 -0700 Subject: [PATCH 02/48] feat: export embedded components and types --- src/embedded/index.ts | 2 ++ src/index.tsx | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/embedded/index.ts b/src/embedded/index.ts index 15eb796c9..967e49dbe 100644 --- a/src/embedded/index.ts +++ b/src/embedded/index.ts @@ -1,2 +1,4 @@ export * from './classes'; +export * from './components'; +export * from './enums'; export * from './types'; diff --git a/src/index.tsx b/src/index.tsx index 75c8489ec..b4ba8f5cf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -32,6 +32,17 @@ export type { IterableEdgeInsetDetails, IterableRetryPolicy, } from './core/types'; +export { + IterableEmbeddedManager, + IterableEmbeddedView, + IterableEmbeddedViewType, + type IterableEmbeddedComponentProps, + type IterableEmbeddedMessage, + type IterableEmbeddedMessageElements, + type IterableEmbeddedMessageElementsButton, + type IterableEmbeddedMessageElementsText, + type IterableEmbeddedViewConfig, +} from './embedded'; export { IterableHtmlInAppContent, IterableInAppCloseSource, @@ -59,7 +70,3 @@ export { type IterableInboxProps, type IterableInboxRowViewModel, } from './inbox'; -export { - IterableEmbeddedManager, - type IterableEmbeddedMessage, -} from './embedded'; From 8b6f82cfffc6add531f8f937e1c52f27ef4e90be Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 16:50:28 -0700 Subject: [PATCH 03/48] feat: enhance Embedded component with view type selection and styling --- .../components/Embedded/Embedded.styles.ts | 38 +++- example/src/components/Embedded/Embedded.tsx | 199 +++++++++--------- 2 files changed, 139 insertions(+), 98 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 56241c676..0574484ea 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -1,5 +1,12 @@ import { StyleSheet } from 'react-native'; -import { button, buttonText, container, hr, link } from '../../constants'; +import { + button, + buttonText, + container, + hr, + link, + colors, +} from '../../constants'; const styles = StyleSheet.create({ button, @@ -26,6 +33,35 @@ const styles = StyleSheet.create({ utilitySection: { paddingHorizontal: 16, }, + viewTypeButton: { + alignItems: 'center', + borderColor: colors.brandCyan, + borderRadius: 8, + borderWidth: 2, + flex: 1, + paddingHorizontal: 12, + paddingVertical: 8, + }, + viewTypeButtonSelected: { + backgroundColor: colors.brandCyan, + }, + viewTypeButtonText: { + color: colors.brandCyan, + fontSize: 14, + fontWeight: '600', + }, + viewTypeButtonTextSelected: { + color: colors.backgroundPrimary, + }, + viewTypeButtons: { + flexDirection: 'row', + gap: 8, + justifyContent: 'space-around', + marginTop: 8, + }, + viewTypeSelector: { + marginVertical: 12, + }, }); export default styles; diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 68b748048..1b0f8ce53 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -2,8 +2,10 @@ import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useCallback, useState } from 'react'; import { Iterable, - type IterableAction, + // type IterableAction, type IterableEmbeddedMessage, + IterableEmbeddedView, + IterableEmbeddedViewType, } from '@iterable/react-native-sdk'; import styles from './Embedded.styles'; @@ -13,6 +15,8 @@ export const Embedded = () => { const [embeddedMessages, setEmbeddedMessages] = useState< IterableEmbeddedMessage[] >([]); + const [selectedViewType, setSelectedViewType] = + useState(IterableEmbeddedViewType.Banner); const syncEmbeddedMessages = useCallback(() => { Iterable.embeddedManager.syncMessages(); @@ -49,52 +53,108 @@ export const Embedded = () => { }); }, [getPlacementIds]); - const startEmbeddedImpression = useCallback( - (message: IterableEmbeddedMessage) => { - console.log(`startEmbeddedImpression`, message); - Iterable.embeddedManager.startImpression( - message.metadata.messageId, - // TODO: check if this should be changed to a number, as per the type - Number(message.metadata.placementId) - ); - }, - [] - ); + // const startEmbeddedImpression = useCallback( + // (message: IterableEmbeddedMessage) => { + // console.log(`startEmbeddedImpression`, message); + // Iterable.embeddedManager.startImpression( + // message.metadata.messageId, + // // TODO: check if this should be changed to a number, as per the type + // Number(message.metadata.placementId) + // ); + // }, + // [] + // ); - const pauseEmbeddedImpression = useCallback( - (message: IterableEmbeddedMessage) => { - console.log(`pauseEmbeddedImpression:`, message); - Iterable.embeddedManager.pauseImpression(message.metadata.messageId); - }, - [] - ); + // const pauseEmbeddedImpression = useCallback( + // (message: IterableEmbeddedMessage) => { + // console.log(`pauseEmbeddedImpression:`, message); + // Iterable.embeddedManager.pauseImpression(message.metadata.messageId); + // }, + // [] + // ); - const handleClick = useCallback( - ( - message: IterableEmbeddedMessage, - buttonId: string | null, - action?: IterableAction | null - ) => { - console.log(`handleClick:`, message); - Iterable.embeddedManager.handleClick(message, buttonId, action); - }, - [] - ); + // const handleClick = useCallback( + // ( + // message: IterableEmbeddedMessage, + // buttonId: string | null, + // action?: IterableAction | null + // ) => { + // console.log(`handleClick:`, message); + // Iterable.embeddedManager.handleClick(message, buttonId, action); + // }, + // [] + // ); return ( EMBEDDED - - Does embedded class exist? {Iterable.embeddedManager ? 'Yes' : 'No'} - - - Is embedded manager enabled? - {Iterable.embeddedManager.isEnabled ? 'Yes' : 'No'} - Placement ids: [{placementIds.join(', ')}] + + Select View Type: + + + setSelectedViewType(IterableEmbeddedViewType.Banner) + } + > + + Banner + + + setSelectedViewType(IterableEmbeddedViewType.Card)} + > + + Card + + + + setSelectedViewType(IterableEmbeddedViewType.Notification) + } + > + + Notification + + + + Sync messages @@ -115,66 +175,11 @@ export const Embedded = () => { {embeddedMessages.map((message) => ( - - - Embedded message - - - startEmbeddedImpression(message)} - > - Start impression - - | - pauseEmbeddedImpression(message)} - > - Pause impression - - | - - handleClick(message, null, message.elements?.defaultAction) - } - > - Handle click - - - - metadata.messageId: {message.metadata.messageId} - metadata.placementId: {message.metadata.placementId} - elements.title: {message.elements?.title} - elements.body: {message.elements?.body} - - elements.defaultAction.data:{' '} - {message.elements?.defaultAction?.data} - - - elements.defaultAction.type:{' '} - {message.elements?.defaultAction?.type} - - {(message.elements?.buttons ?? []).map((button, buttonIndex) => ( - - - Button {buttonIndex + 1} - | - - handleClick(message, button.id, button.action) - } - > - Handle click - - - - button.id: {button.id} - button.title: {button.title} - button.action?.data: {button.action?.data} - button.action?.type: {button.action?.type} - - ))} - payload: {JSON.stringify(message.payload)} - + ))} From d74383f53c6a676e99f23b6812bda21c4ea0b7f1 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 10 Feb 2026 14:45:39 -0800 Subject: [PATCH 04/48] refactor: clean up Embedded component by removing commented-out code and unused styles --- .../components/Embedded/Embedded.styles.ts | 19 ++++------- example/src/components/Embedded/Embedded.tsx | 33 ------------------- 2 files changed, 6 insertions(+), 46 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 27b2192ed..a1e4f6257 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -1,13 +1,16 @@ import { StyleSheet } from 'react-native'; import { + backgroundColors, button, buttonText, + colors, container, hr, - link, input, title, subtitle, - colors, + input, + subtitle, + title, + utilityColors, } from '../../constants'; -import { utilityColors, backgroundColors } from '../../constants/styles/colors'; const styles = StyleSheet.create({ button, @@ -19,20 +22,10 @@ const styles = StyleSheet.create({ gap: 16, paddingHorizontal: 16, }, - embeddedTitle: { - fontSize: 16, - fontWeight: 'bold', - lineHeight: 20, - }, - embeddedTitleContainer: { - display: 'flex', - flexDirection: 'row', - }, hr, inputContainer: { marginVertical: 10, }, - link, subtitle: { ...subtitle, textAlign: 'center' }, text: { textAlign: 'center' }, textInput: input, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 515b7b028..4ac18a6f4 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -8,7 +8,6 @@ import { import { useCallback, useState } from 'react'; import { Iterable, - // type IterableAction, type IterableEmbeddedMessage, IterableEmbeddedView, IterableEmbeddedViewType, @@ -56,35 +55,6 @@ export const Embedded = () => { }); }, [idsToFetch]); - // const startEmbeddedImpression = useCallback( - // (message: IterableEmbeddedMessage) => { - // Iterable.embeddedManager.startImpression( - // message.metadata.messageId, - // // TODO: check if this should be changed to a number, as per the type - // Number(message.metadata.placementId) - // ); - // }, - // [] - // ); - - // const pauseEmbeddedImpression = useCallback( - // (message: IterableEmbeddedMessage) => { - // Iterable.embeddedManager.pauseImpression(message.metadata.messageId); - // }, - // [] - // ); - - // const handleClick = useCallback( - // ( - // message: IterableEmbeddedMessage, - // buttonId: string | null, - // action?: IterableAction | null - // ) => { - // Iterable.embeddedManager.handleClick(message, buttonId, action); - // }, - // [] - // ); - return ( Embedded @@ -100,9 +70,6 @@ export const Embedded = () => { Enter placement IDs to fetch embedded messages - {/* - Placement ids: [{placementIds.join(', ')}] - */} Select View Type: From a8d300862d91e75f384b009ee81726ae5c30e8f4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 10 Feb 2026 15:02:57 -0800 Subject: [PATCH 05/48] test: add unit tests for IterableEmbeddedView component rendering and props passing --- .../components/IterableEmbeddedView.test.tsx | 373 ++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedView.test.tsx diff --git a/src/embedded/components/IterableEmbeddedView.test.tsx b/src/embedded/components/IterableEmbeddedView.test.tsx new file mode 100644 index 000000000..4bcc47dcf --- /dev/null +++ b/src/embedded/components/IterableEmbeddedView.test.tsx @@ -0,0 +1,373 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { render } from '@testing-library/react-native'; + +import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; +import { IterableEmbeddedView } from './IterableEmbeddedView'; +import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; +import { IterableEmbeddedCard } from './IterableEmbeddedCard'; +import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; + +// Mock the child components +jest.mock('./IterableEmbeddedBanner', () => ({ + IterableEmbeddedBanner: jest.fn(() => null), +})); + +jest.mock('./IterableEmbeddedCard', () => ({ + IterableEmbeddedCard: jest.fn(() => null), +})); + +jest.mock('./IterableEmbeddedNotification', () => ({ + IterableEmbeddedNotification: jest.fn(() => null), +})); + +describe('IterableEmbeddedView', () => { + const mockMessage = { + metadata: { + messageId: 'test-message-123', + campaignId: 123456, + placementId: 'test-placement', + }, + elements: { + title: 'Test Title', + body: 'Test Body', + }, + } as any; + + const mockConfig = { + backgroundColor: '#FFFFFF', + borderRadius: 8, + } as any; + + const mockOnButtonClick = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('View Type Rendering', () => { + it('should render IterableEmbeddedCard when viewType is Card', () => { + render( + + ); + + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + expect(IterableEmbeddedBanner).not.toHaveBeenCalled(); + expect(IterableEmbeddedNotification).not.toHaveBeenCalled(); + }); + + it('should render IterableEmbeddedNotification when viewType is Notification', () => { + render( + + ); + + expect(IterableEmbeddedNotification).toHaveBeenCalledTimes(1); + expect(IterableEmbeddedCard).not.toHaveBeenCalled(); + expect(IterableEmbeddedBanner).not.toHaveBeenCalled(); + }); + + it('should render IterableEmbeddedBanner when viewType is Banner', () => { + render( + + ); + + expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); + expect(IterableEmbeddedCard).not.toHaveBeenCalled(); + expect(IterableEmbeddedNotification).not.toHaveBeenCalled(); + }); + + it('should render null for invalid viewType', () => { + render( + + ); + + expect(IterableEmbeddedCard).not.toHaveBeenCalled(); + expect(IterableEmbeddedBanner).not.toHaveBeenCalled(); + expect(IterableEmbeddedNotification).not.toHaveBeenCalled(); + }); + + it('should render null for undefined viewType', () => { + render( + + ); + + expect(IterableEmbeddedCard).not.toHaveBeenCalled(); + expect(IterableEmbeddedBanner).not.toHaveBeenCalled(); + expect(IterableEmbeddedNotification).not.toHaveBeenCalled(); + }); + }); + + describe('Props Passing', () => { + it('should pass message prop to Card component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + }); + }); + + it('should pass message prop to Banner component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedBanner as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + }); + }); + + it('should pass message prop to Notification component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedNotification as jest.Mock).mock + .calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + }); + }); + + it('should pass config prop to child component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + config: mockConfig, + }); + }); + + it('should pass onButtonClick prop to child component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + onButtonClick: mockOnButtonClick, + }); + }); + + it('should pass all props to child component', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + config: mockConfig, + onButtonClick: mockOnButtonClick, + }); + }); + }); + + describe('Component Memoization', () => { + it('should memoize component selection based on viewType', () => { + const { rerender } = render( + + ); + + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + + // Re-render with same viewType but different message + const newMessage = { + ...mockMessage, + metadata: { + ...mockMessage.metadata, + messageId: 'different-id', + }, + }; + + rerender( + + ); + + // Should still render Card component (memoization means same component reference) + // Card should be called again with new props + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(2); + const lastCallArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[1][0]; + expect(lastCallArgs).toMatchObject({ + message: newMessage, + }); + }); + + it('should update component when viewType changes', () => { + const { rerender } = render( + + ); + + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + expect(IterableEmbeddedBanner).not.toHaveBeenCalled(); + + // Re-render with different viewType + rerender( + + ); + + expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); + // Card was called only once (from initial render) + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + }); + }); + + describe('Edge Cases', () => { + it('should handle null config gracefully', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + config: null, + }); + }); + + it('should handle undefined config gracefully', () => { + render( + + ); + + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + config: undefined, + }); + }); + + it('should handle missing onButtonClick gracefully', () => { + render( + + ); + + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + const callArgs = (IterableEmbeddedCard as jest.Mock).mock.calls[0][0]; + expect(callArgs).toMatchObject({ + message: mockMessage, + }); + }); + + it('should handle numeric viewType values correctly', () => { + // Test with numeric value 0 (Banner) + render(); + expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + // Test with numeric value 1 (Card) + render(); + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + // Test with numeric value 2 (Notification) + render(); + expect(IterableEmbeddedNotification).toHaveBeenCalledTimes(1); + }); + }); + + describe('Component Type Verification', () => { + it('should render correct component type for each enum value', () => { + // Verify Banner enum value + const bannerResult = render( + + ); + expect(IterableEmbeddedBanner).toHaveBeenCalledTimes(1); + bannerResult.unmount(); + + jest.clearAllMocks(); + + // Verify Card enum value + const cardResult = render( + + ); + expect(IterableEmbeddedCard).toHaveBeenCalledTimes(1); + cardResult.unmount(); + + jest.clearAllMocks(); + + // Verify Notification enum value + render( + + ); + expect(IterableEmbeddedNotification).toHaveBeenCalledTimes(1); + }); + }); +}); From ba7fccc24e742cfdb5c5205c6040859bb4688681 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 10 Feb 2026 15:12:16 -0800 Subject: [PATCH 06/48] docs: add JSDoc comments to IterableEmbeddedView component for better clarity on props and usage --- src/embedded/components/IterableEmbeddedView.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index fa76f584f..c123c94b4 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -15,6 +15,17 @@ interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { viewType: IterableEmbeddedViewType; } +/** + * + * @param viewType - The type of view to render. + * @param message - The message to render. + * @param config - The config for the IterableEmbeddedView component, most likely used to style the view. + * @param onButtonClick - The function to call when a button is clicked. + * @returns The IterableEmbeddedView component. + * + * This component is used to render pre-created, customizable message displays + * included with Iterables RN SDK: cards, banners, and notifications. + */ export const IterableEmbeddedView = ({ viewType, ...props From 95e8d92311aa76108bfac582979714684f904b3d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 12 Feb 2026 14:10:59 -0800 Subject: [PATCH 07/48] feat: add IterableEmbeddedNotification component for displaying notifications --- .../IterableEmbeddedNotification.tsx | 2 +- src/embedded/components/IterableEmbeddedNotification/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) rename src/embedded/components/{ => IterableEmbeddedNotification}/IterableEmbeddedNotification.tsx (84%) create mode 100644 src/embedded/components/IterableEmbeddedNotification/index.ts diff --git a/src/embedded/components/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx similarity index 84% rename from src/embedded/components/IterableEmbeddedNotification.tsx rename to src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index 686ea01e6..4ecbc67c7 100644 --- a/src/embedded/components/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -1,6 +1,6 @@ import { View, Text } from 'react-native'; -import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; export const IterableEmbeddedNotification = ({ config, diff --git a/src/embedded/components/IterableEmbeddedNotification/index.ts b/src/embedded/components/IterableEmbeddedNotification/index.ts new file mode 100644 index 000000000..3a25fd8ee --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification/index.ts @@ -0,0 +1,2 @@ +export * from './IterableEmbeddedNotification'; +export { IterableEmbeddedNotification as default } from './IterableEmbeddedNotification'; From ef7cf1de4960d93fc076ad9fd5459111ad6269d5 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 12 Feb 2026 14:29:00 -0800 Subject: [PATCH 08/48] feat: add utility functions and styles for embedded views --- .../IterableEmbeddedNotification.styles.ts | 0 src/embedded/contants/embeddedViewDefaults.ts | 85 +++++++++++++++++++ src/embedded/contants/index.ts | 1 + src/embedded/utils/getMedia.ts | 29 +++++++ src/embedded/utils/getStyles.ts | 81 ++++++++++++++++++ src/embedded/utils/index.ts | 2 + 6 files changed, 198 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts create mode 100644 src/embedded/contants/embeddedViewDefaults.ts create mode 100644 src/embedded/contants/index.ts create mode 100644 src/embedded/utils/getMedia.ts create mode 100644 src/embedded/utils/getStyles.ts create mode 100644 src/embedded/utils/index.ts diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/embedded/contants/embeddedViewDefaults.ts b/src/embedded/contants/embeddedViewDefaults.ts new file mode 100644 index 000000000..bae1c8799 --- /dev/null +++ b/src/embedded/contants/embeddedViewDefaults.ts @@ -0,0 +1,85 @@ +export const embeddedBackgroundColors = { + notification: '#ffffff', + card: '#ffffff', + banner: '#ffffff', +}; + +export const embeddedBorderColors = { + notification: '#E0DEDF', + card: '#E0DEDF', + banner: '#E0DEDF', +}; + +export const embeddedPrimaryBtnBackgroundColors = { + notification: '#6A266D', + card: 'transparent', + banner: '#6A266D', +}; + +export const embeddedPrimaryBtnTextColors = { + notification: '#ffffff', + card: '#79347F', + banner: '#ffffff', +}; + +export const embeddedSecondaryBtnBackgroundColors = { + notification: 'transparent', + card: 'transparent', + banner: 'transparent', +}; + +export const embeddedSecondaryBtnTextColors = { + notification: '#79347F', + card: '#79347F', + banner: '#79347F', +}; + +export const embeddedTitleTextColors = { + notification: '#3D3A3B', + card: '#3D3A3B', + banner: '#3D3A3B', +}; + +export const embeddedBodyTextColors = { + notification: '#787174', + card: '#787174', + banner: '#787174', +}; + +export const embeddedBorderRadius = { + notification: 8, + card: 6, + banner: 8, +}; + +export const embeddedBorderWidth = { + notification: 1, + card: 1, + banner: 1, +}; + +export const embeddedMediaImageBorderColors = { + notification: '#E0DEDF', + card: '#E0DEDF', + banner: '#E0DEDF', +}; + +export const embeddedMediaImageBackgroundColors = { + notification: '#F5F4F4', + card: '#F5F4F4', + banner: '#F5F4F4', +}; + +export const embeddedStyles = { + background: embeddedBackgroundColors, + border: embeddedBorderColors, + primaryBtnBackground: embeddedPrimaryBtnBackgroundColors, + primaryBtnText: embeddedPrimaryBtnTextColors, + secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, + secondaryBtnText: embeddedSecondaryBtnTextColors, + titleText: embeddedTitleTextColors, + bodyText: embeddedBodyTextColors, + mediaImageBorder: embeddedMediaImageBorderColors, + borderRadius: embeddedBorderRadius, + borderWidth: embeddedBorderWidth, +}; diff --git a/src/embedded/contants/index.ts b/src/embedded/contants/index.ts new file mode 100644 index 000000000..4324689be --- /dev/null +++ b/src/embedded/contants/index.ts @@ -0,0 +1 @@ +export * from './embeddedViewDefaults'; diff --git a/src/embedded/utils/getMedia.ts b/src/embedded/utils/getMedia.ts new file mode 100644 index 000000000..a9c1325c2 --- /dev/null +++ b/src/embedded/utils/getMedia.ts @@ -0,0 +1,29 @@ +import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; +import { IterableEmbeddedViewType } from '../enums'; + +/** + * This function is used to get the media to render for a given embedded view + * type and message. + * + * @param viewType - The type of view to render. + * @param message - The message to render. + * @returns The media to render. + * + * @example + * const media = getMedia(IterableEmbeddedViewType.Notification, message); + * console.log(media); + */ +export const getMedia = ( + /** The type of view to render. */ + viewType: IterableEmbeddedViewType, + /** The message to render. */ + message: IterableEmbeddedMessage +) => { + if (viewType === IterableEmbeddedViewType.Notification) { + return { url: null, caption: null, shouldShow: false }; + } + const url = message.elements?.mediaUrl ?? null; + const caption = message.elements?.mediaUrlCaption ?? null; + const shouldShow = !!url && url.length > 0; + return { url, caption, shouldShow }; +}; diff --git a/src/embedded/utils/getStyles.ts b/src/embedded/utils/getStyles.ts new file mode 100644 index 000000000..e233ce96b --- /dev/null +++ b/src/embedded/utils/getStyles.ts @@ -0,0 +1,81 @@ +import type { IterableEmbeddedViewConfig } from '../types/IterableEmbeddedViewConfig'; +import { embeddedStyles } from '../contants/embeddedViewDefaults'; +import { IterableEmbeddedViewType } from '../enums'; + +/** + * Get the default style for the embedded view type. + * + * @param viewType - The type of view to render. + * @param colors - The colors to use for the default style. + * @returns The default style. + */ +const getDefaultStyle = ( + viewType: IterableEmbeddedViewType, + colors: { + banner: number | string; + card: number | string; + notification: number | string; + } +) => { + switch (viewType) { + case IterableEmbeddedViewType.Notification: + return colors.notification; + case IterableEmbeddedViewType.Card: + return colors.card; + default: + return colors.banner; + } +}; + +/** + * Get the style for the embedded view type. + * + * If a style is provided in the config, it will take precedence over the default style. + * + * @param viewType - The type of view to render. + * @param c - The config to use for the styles. + * @returns The styles. + * + * @example + * const styles = getStyles(IterableEmbeddedViewType.Notification, { + * backgroundColor: '#000000', + * borderColor: '#000000', + * borderWidth: 1, + * borderCornerRadius: 10, + * primaryBtnBackgroundColor: '#000000', + * primaryBtnTextColor: '#000000', + * }); + */ +export const getStyles = ( + viewType: IterableEmbeddedViewType, + c?: IterableEmbeddedViewConfig | null +) => { + return { + backgroundColor: + c?.backgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.background), + borderColor: + c?.borderColor ?? getDefaultStyle(viewType, embeddedStyles.border), + borderWidth: + c?.borderWidth ?? getDefaultStyle(viewType, embeddedStyles.borderWidth), + borderCornerRadius: + c?.borderCornerRadius ?? + getDefaultStyle(viewType, embeddedStyles.borderRadius), + primaryBtnBackgroundColor: + c?.primaryBtnBackgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.primaryBtnBackground), + primaryBtnTextColor: + c?.primaryBtnTextColor ?? + getDefaultStyle(viewType, embeddedStyles.primaryBtnText), + secondaryBtnBackgroundColor: + c?.secondaryBtnBackgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.secondaryBtnBackground), + secondaryBtnTextColor: + c?.secondaryBtnTextColor ?? + getDefaultStyle(viewType, embeddedStyles.secondaryBtnText), + titleTextColor: + c?.titleTextColor ?? getDefaultStyle(viewType, embeddedStyles.titleText), + bodyTextColor: + c?.bodyTextColor ?? getDefaultStyle(viewType, embeddedStyles.bodyText), + }; +}; diff --git a/src/embedded/utils/index.ts b/src/embedded/utils/index.ts new file mode 100644 index 000000000..d51bbbedf --- /dev/null +++ b/src/embedded/utils/index.ts @@ -0,0 +1,2 @@ +export * from './getMedia'; +export * from './getStyles'; From 980123dd72c666778ddc66708671e441b7cd76f7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 12 Feb 2026 14:35:35 -0800 Subject: [PATCH 09/48] feat: implement useEmbeddedView hook for managing embedded view lifecycle --- src/embedded/hooks/index.ts | 1 + src/embedded/hooks/useEmbeddedView.ts | 74 +++++++++++++++++++ .../types/IterableEmbeddedComponentProps.ts | 5 ++ 3 files changed, 80 insertions(+) create mode 100644 src/embedded/hooks/index.ts create mode 100644 src/embedded/hooks/useEmbeddedView.ts diff --git a/src/embedded/hooks/index.ts b/src/embedded/hooks/index.ts new file mode 100644 index 000000000..cbca753d9 --- /dev/null +++ b/src/embedded/hooks/index.ts @@ -0,0 +1 @@ +export * from './useEmbeddedView'; diff --git a/src/embedded/hooks/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView.ts new file mode 100644 index 000000000..c8effccbd --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView.ts @@ -0,0 +1,74 @@ +import { useCallback, useMemo } from 'react'; +import { Iterable } from '../../core/classes/Iterable'; +import { IterableEmbeddedViewType } from '../enums'; +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; +import type { IterableEmbeddedMessageElementsButton } from '../types/IterableEmbeddedMessageElementsButton'; +import { getMedia } from '../utils/getMedia'; +import { getStyles } from '../utils/getStyles'; + +/** + * This hook is used to manage the lifecycle of an embedded view. + * + * @param viewType - The type of view to render. + * @param props - The props for the embedded view. + * @returns The embedded view. + * + * @example + * const { handleButtonClick, handleMessageClick, media, parsedStyles } = useEmbeddedView(IterableEmbeddedViewType.Notification, { + * message, + * config, + * onButtonClick, + * onMessageClick, + * }); + * + * return ( + * + * {media.url} + * {media.caption} + * {parsedStyles.backgroundColor} + * + * ); + */ +export const useEmbeddedView = ( + /** The type of view to render. */ + viewType: IterableEmbeddedViewType, + /** The props for the embedded view. */ + { + message, + config, + onButtonClick = () => {}, + onMessageClick = () => {}, + }: IterableEmbeddedComponentProps +) => { + const parsedStyles = useMemo(() => { + return getStyles(viewType, config); + }, [viewType, config]); + const media = useMemo(() => { + return getMedia(viewType, message); + }, [viewType, message]); + + + const handleButtonClick = useCallback( + (button: IterableEmbeddedMessageElementsButton) => { + onButtonClick(button); + Iterable.embeddedManager.handleClick(message, button.id, button.action); + }, + [onButtonClick, message] + ); + + const handleMessageClick = useCallback(() => { + onMessageClick(); + Iterable.embeddedManager.handleClick( + message, + null, + message.elements?.defaultAction + ); + }, [message, onMessageClick]); + + return { + handleButtonClick, + handleMessageClick, + media, + parsedStyles, + }; +}; diff --git a/src/embedded/types/IterableEmbeddedComponentProps.ts b/src/embedded/types/IterableEmbeddedComponentProps.ts index 9f2b17670..f59e2772e 100644 --- a/src/embedded/types/IterableEmbeddedComponentProps.ts +++ b/src/embedded/types/IterableEmbeddedComponentProps.ts @@ -3,7 +3,12 @@ import type { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMe import type { IterableEmbeddedViewConfig } from './IterableEmbeddedViewConfig'; export interface IterableEmbeddedComponentProps { + /** The message to render. */ message: IterableEmbeddedMessage; + /** The config for the embedded view. */ config?: IterableEmbeddedViewConfig | null; + /** The function to call when a button is clicked. */ onButtonClick?: (button: IterableEmbeddedMessageElementsButton) => void; + /** The function to call when the message is clicked. */ + onMessageClick?: () => void; } From 917835b6cc58ed8f16c3c224ba6f61c06846242a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 12 Feb 2026 14:37:30 -0800 Subject: [PATCH 10/48] feat: enhance IterableEmbeddedNotification with styles and button functionality --- .../IterableEmbeddedNotification.styles.ts | 54 +++++++++++ .../IterableEmbeddedNotification.tsx | 94 +++++++++++++++++-- 2 files changed, 138 insertions(+), 10 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts index e69de29bb..923df66fc 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts @@ -0,0 +1,54 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + gap: 4, + width: '100%', + }, + button: { + borderRadius: 32, + gap: 8, + paddingHorizontal: 12, + paddingVertical: 8, + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '700', + lineHeight: 20, + }, + container: { + alignItems: 'flex-start', + borderStyle: 'solid', + boxShadow: + '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + gap: 8, + justifyContent: 'center', + padding: 16, + width: '100%', + }, + title: { + fontSize: 16, + fontWeight: '700', + lineHeight: 24, + }, +}); diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index 4ecbc67c7..f0909cfc5 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -1,22 +1,96 @@ -import { View, Text } from 'react-native'; +import { + Text, + TouchableOpacity, + View, + type TextStyle, + type ViewStyle, + Pressable, +} from 'react-native'; +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import { styles } from './IterableEmbeddedNotification.styles'; export const IterableEmbeddedNotification = ({ config, message, onButtonClick = () => {}, + onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { - console.log(`🚀 > IterableEmbeddedNotification > config:`, config); - console.log(`🚀 > IterableEmbeddedNotification > message:`, message); - console.log( - `🚀 > IterableEmbeddedNotification > onButtonClick:`, - onButtonClick - ); + const { parsedStyles, handleButtonClick, handleMessageClick } = + useEmbeddedView(IterableEmbeddedViewType.Notification, { + message, + config, + onButtonClick, + onMessageClick, + }); + + const buttons = message.elements?.buttons ?? []; return ( - - IterableEmbeddedNotification - + handleMessageClick()}> + + {} + + + {message.elements?.title} + + + {message.elements?.body} + + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + handleButtonClick(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} + + ); }; From 4a47ef20e31cb883070c7a522cfdd317870b2f51 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 12 Feb 2026 15:02:11 -0800 Subject: [PATCH 11/48] feat: add hooks for managing embedded view styles and media --- src/embedded/contants/index.ts | 1 - .../useEmbeddedView}/embeddedViewDefaults.ts | 18 +++++++++--------- .../useEmbeddedView}/getMedia.ts | 4 ++-- .../useEmbeddedView}/getStyles.ts | 18 +++++++++--------- src/embedded/hooks/useEmbeddedView/index.ts | 2 ++ .../{ => useEmbeddedView}/useEmbeddedView.ts | 12 ++++++------ src/embedded/index.ts | 2 ++ src/embedded/utils/index.ts | 2 -- 8 files changed, 30 insertions(+), 29 deletions(-) delete mode 100644 src/embedded/contants/index.ts rename src/embedded/{contants => hooks/useEmbeddedView}/embeddedViewDefaults.ts (84%) rename src/embedded/{utils => hooks/useEmbeddedView}/getMedia.ts (85%) rename src/embedded/{utils => hooks/useEmbeddedView}/getStyles.ts (81%) create mode 100644 src/embedded/hooks/useEmbeddedView/index.ts rename src/embedded/hooks/{ => useEmbeddedView}/useEmbeddedView.ts (80%) delete mode 100644 src/embedded/utils/index.ts diff --git a/src/embedded/contants/index.ts b/src/embedded/contants/index.ts deleted file mode 100644 index 4324689be..000000000 --- a/src/embedded/contants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './embeddedViewDefaults'; diff --git a/src/embedded/contants/embeddedViewDefaults.ts b/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts similarity index 84% rename from src/embedded/contants/embeddedViewDefaults.ts rename to src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts index bae1c8799..f20879388 100644 --- a/src/embedded/contants/embeddedViewDefaults.ts +++ b/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts @@ -71,15 +71,15 @@ export const embeddedMediaImageBackgroundColors = { }; export const embeddedStyles = { - background: embeddedBackgroundColors, - border: embeddedBorderColors, - primaryBtnBackground: embeddedPrimaryBtnBackgroundColors, - primaryBtnText: embeddedPrimaryBtnTextColors, - secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, - secondaryBtnText: embeddedSecondaryBtnTextColors, - titleText: embeddedTitleTextColors, + backgroundColor: embeddedBackgroundColors, bodyText: embeddedBodyTextColors, - mediaImageBorder: embeddedMediaImageBorderColors, - borderRadius: embeddedBorderRadius, + borderColor: embeddedBorderColors, + borderCornerRadius: embeddedBorderRadius, borderWidth: embeddedBorderWidth, + mediaImageBorder: embeddedMediaImageBorderColors, + primaryBtnBackgroundColor: embeddedPrimaryBtnBackgroundColors, + primaryBtnTextColor: embeddedPrimaryBtnTextColors, + secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, + secondaryBtnTextColor: embeddedSecondaryBtnTextColors, + titleText: embeddedTitleTextColors, }; diff --git a/src/embedded/utils/getMedia.ts b/src/embedded/hooks/useEmbeddedView/getMedia.ts similarity index 85% rename from src/embedded/utils/getMedia.ts rename to src/embedded/hooks/useEmbeddedView/getMedia.ts index a9c1325c2..de0dcb666 100644 --- a/src/embedded/utils/getMedia.ts +++ b/src/embedded/hooks/useEmbeddedView/getMedia.ts @@ -1,5 +1,5 @@ -import type { IterableEmbeddedMessage } from '../types/IterableEmbeddedMessage'; -import { IterableEmbeddedViewType } from '../enums'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import { IterableEmbeddedViewType } from '../../enums'; /** * This function is used to get the media to render for a given embedded view diff --git a/src/embedded/utils/getStyles.ts b/src/embedded/hooks/useEmbeddedView/getStyles.ts similarity index 81% rename from src/embedded/utils/getStyles.ts rename to src/embedded/hooks/useEmbeddedView/getStyles.ts index e233ce96b..102367de5 100644 --- a/src/embedded/utils/getStyles.ts +++ b/src/embedded/hooks/useEmbeddedView/getStyles.ts @@ -1,6 +1,6 @@ -import type { IterableEmbeddedViewConfig } from '../types/IterableEmbeddedViewConfig'; -import { embeddedStyles } from '../contants/embeddedViewDefaults'; -import { IterableEmbeddedViewType } from '../enums'; +import type { IterableEmbeddedViewConfig } from '../../types/IterableEmbeddedViewConfig'; +import { embeddedStyles } from './embeddedViewDefaults'; +import { IterableEmbeddedViewType } from '../../enums'; /** * Get the default style for the embedded view type. @@ -53,26 +53,26 @@ export const getStyles = ( return { backgroundColor: c?.backgroundColor ?? - getDefaultStyle(viewType, embeddedStyles.background), + getDefaultStyle(viewType, embeddedStyles.backgroundColor), borderColor: - c?.borderColor ?? getDefaultStyle(viewType, embeddedStyles.border), + c?.borderColor ?? getDefaultStyle(viewType, embeddedStyles.borderColor), borderWidth: c?.borderWidth ?? getDefaultStyle(viewType, embeddedStyles.borderWidth), borderCornerRadius: c?.borderCornerRadius ?? - getDefaultStyle(viewType, embeddedStyles.borderRadius), + getDefaultStyle(viewType, embeddedStyles.borderCornerRadius), primaryBtnBackgroundColor: c?.primaryBtnBackgroundColor ?? - getDefaultStyle(viewType, embeddedStyles.primaryBtnBackground), + getDefaultStyle(viewType, embeddedStyles.primaryBtnBackgroundColor), primaryBtnTextColor: c?.primaryBtnTextColor ?? - getDefaultStyle(viewType, embeddedStyles.primaryBtnText), + getDefaultStyle(viewType, embeddedStyles.primaryBtnTextColor), secondaryBtnBackgroundColor: c?.secondaryBtnBackgroundColor ?? getDefaultStyle(viewType, embeddedStyles.secondaryBtnBackground), secondaryBtnTextColor: c?.secondaryBtnTextColor ?? - getDefaultStyle(viewType, embeddedStyles.secondaryBtnText), + getDefaultStyle(viewType, embeddedStyles.secondaryBtnTextColor), titleTextColor: c?.titleTextColor ?? getDefaultStyle(viewType, embeddedStyles.titleText), bodyTextColor: diff --git a/src/embedded/hooks/useEmbeddedView/index.ts b/src/embedded/hooks/useEmbeddedView/index.ts new file mode 100644 index 000000000..bf1a77d44 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/index.ts @@ -0,0 +1,2 @@ +export * from './useEmbeddedView'; +export { useEmbeddedView as default } from './useEmbeddedView'; diff --git a/src/embedded/hooks/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts similarity index 80% rename from src/embedded/hooks/useEmbeddedView.ts rename to src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index c8effccbd..cfbd9fc4f 100644 --- a/src/embedded/hooks/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -1,10 +1,10 @@ import { useCallback, useMemo } from 'react'; -import { Iterable } from '../../core/classes/Iterable'; -import { IterableEmbeddedViewType } from '../enums'; -import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; -import type { IterableEmbeddedMessageElementsButton } from '../types/IterableEmbeddedMessageElementsButton'; -import { getMedia } from '../utils/getMedia'; -import { getStyles } from '../utils/getStyles'; +import { Iterable } from '../../../core/classes/Iterable'; +import { IterableEmbeddedViewType } from '../../enums'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton'; +import { getMedia } from './getMedia'; +import { getStyles } from './getStyles'; /** * This hook is used to manage the lifecycle of an embedded view. diff --git a/src/embedded/index.ts b/src/embedded/index.ts index 967e49dbe..107bb59fe 100644 --- a/src/embedded/index.ts +++ b/src/embedded/index.ts @@ -1,4 +1,6 @@ export * from './classes'; export * from './components'; export * from './enums'; +export * from './hooks'; export * from './types'; + diff --git a/src/embedded/utils/index.ts b/src/embedded/utils/index.ts deleted file mode 100644 index d51bbbedf..000000000 --- a/src/embedded/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './getMedia'; -export * from './getStyles'; From fb1e938852f0f027c593c8d113f1c6fa8a054638 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:14:40 -0800 Subject: [PATCH 12/48] feat: implement useEmbeddedView hook and associated styles for embedded components --- .../components/IterableEmbeddedView.tsx | 21 ++++- src/embedded/hooks/index.ts | 1 + .../useEmbeddedView/embeddedViewDefaults.ts | 85 +++++++++++++++++++ .../hooks/useEmbeddedView/getStyles.ts | 81 ++++++++++++++++++ src/embedded/hooks/useEmbeddedView/index.ts | 2 + .../hooks/useEmbeddedView/useEmbeddedView.ts | 42 +++++++++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/embedded/hooks/index.ts create mode 100644 src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts create mode 100644 src/embedded/hooks/useEmbeddedView/getStyles.ts create mode 100644 src/embedded/hooks/useEmbeddedView/index.ts create mode 100644 src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index c123c94b4..91ec5e355 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { View, Text } from 'react-native'; import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; @@ -6,6 +7,7 @@ import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; import { IterableEmbeddedCard } from './IterableEmbeddedCard'; import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; +import { useEmbeddedView } from '../hooks/useEmbeddedView/useEmbeddedView'; /** * The props for the IterableEmbeddedView component. @@ -43,5 +45,22 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - return Cmp ? : null; + const { parsedStyles } = + useEmbeddedView(IterableEmbeddedViewType.Notification, props); + + return Cmp ? ( + + parsedStyles.backgroundColor: {String(parsedStyles.backgroundColor)} + parsedStyles.borderColor: {String(parsedStyles.borderColor)} + parsedStyles.borderWidth: {parsedStyles.borderWidth} + parsedStyles.borderCornerRadius: {parsedStyles.borderCornerRadius} + parsedStyles.primaryBtnBackgroundColor: {String(parsedStyles.primaryBtnBackgroundColor)} + parsedStyles.primaryBtnTextColor: {String(parsedStyles.primaryBtnTextColor)} + parsedStyles.secondaryBtnBackgroundColor: {String(parsedStyles.secondaryBtnBackgroundColor)} + parsedStyles.secondaryBtnTextColor: {String(parsedStyles.secondaryBtnTextColor)} + parsedStyles.titleTextColor: {String(parsedStyles.titleTextColor)} + parsedStyles.bodyTextColor: {String(parsedStyles.bodyTextColor)} + + + ) : null; }; diff --git a/src/embedded/hooks/index.ts b/src/embedded/hooks/index.ts new file mode 100644 index 000000000..cbca753d9 --- /dev/null +++ b/src/embedded/hooks/index.ts @@ -0,0 +1 @@ +export * from './useEmbeddedView'; diff --git a/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts b/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts new file mode 100644 index 000000000..f20879388 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts @@ -0,0 +1,85 @@ +export const embeddedBackgroundColors = { + notification: '#ffffff', + card: '#ffffff', + banner: '#ffffff', +}; + +export const embeddedBorderColors = { + notification: '#E0DEDF', + card: '#E0DEDF', + banner: '#E0DEDF', +}; + +export const embeddedPrimaryBtnBackgroundColors = { + notification: '#6A266D', + card: 'transparent', + banner: '#6A266D', +}; + +export const embeddedPrimaryBtnTextColors = { + notification: '#ffffff', + card: '#79347F', + banner: '#ffffff', +}; + +export const embeddedSecondaryBtnBackgroundColors = { + notification: 'transparent', + card: 'transparent', + banner: 'transparent', +}; + +export const embeddedSecondaryBtnTextColors = { + notification: '#79347F', + card: '#79347F', + banner: '#79347F', +}; + +export const embeddedTitleTextColors = { + notification: '#3D3A3B', + card: '#3D3A3B', + banner: '#3D3A3B', +}; + +export const embeddedBodyTextColors = { + notification: '#787174', + card: '#787174', + banner: '#787174', +}; + +export const embeddedBorderRadius = { + notification: 8, + card: 6, + banner: 8, +}; + +export const embeddedBorderWidth = { + notification: 1, + card: 1, + banner: 1, +}; + +export const embeddedMediaImageBorderColors = { + notification: '#E0DEDF', + card: '#E0DEDF', + banner: '#E0DEDF', +}; + +export const embeddedMediaImageBackgroundColors = { + notification: '#F5F4F4', + card: '#F5F4F4', + banner: '#F5F4F4', +}; + +export const embeddedStyles = { + backgroundColor: embeddedBackgroundColors, + bodyText: embeddedBodyTextColors, + borderColor: embeddedBorderColors, + borderCornerRadius: embeddedBorderRadius, + borderWidth: embeddedBorderWidth, + mediaImageBorder: embeddedMediaImageBorderColors, + primaryBtnBackgroundColor: embeddedPrimaryBtnBackgroundColors, + primaryBtnTextColor: embeddedPrimaryBtnTextColors, + secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, + secondaryBtnTextColor: embeddedSecondaryBtnTextColors, + titleText: embeddedTitleTextColors, +}; diff --git a/src/embedded/hooks/useEmbeddedView/getStyles.ts b/src/embedded/hooks/useEmbeddedView/getStyles.ts new file mode 100644 index 000000000..16aa2f616 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/getStyles.ts @@ -0,0 +1,81 @@ +import type { IterableEmbeddedViewConfig } from '../../types/IterableEmbeddedViewConfig'; +import { embeddedStyles } from './embeddedViewDefaults'; +import { IterableEmbeddedViewType } from '../../enums'; + +/** + * Get the default style for the embedded view type. + * + * @param viewType - The type of view to render. + * @param colors - The colors to use for the default style. + * @returns The default style. + */ +const getDefaultStyle = ( + viewType: IterableEmbeddedViewType, + colors: { + banner: T; + card: T; + notification: T; + } +): T => { + switch (viewType) { + case IterableEmbeddedViewType.Notification: + return colors.notification; + case IterableEmbeddedViewType.Card: + return colors.card; + default: + return colors.banner; + } +}; + +/** + * Get the style for the embedded view type. + * + * If a style is provided in the config, it will take precedence over the default style. + * + * @param viewType - The type of view to render. + * @param c - The config to use for the styles. + * @returns The styles. + * + * @example + * const styles = getStyles(IterableEmbeddedViewType.Notification, { + * backgroundColor: '#000000', + * borderColor: '#000000', + * borderWidth: 1, + * borderCornerRadius: 10, + * primaryBtnBackgroundColor: '#000000', + * primaryBtnTextColor: '#000000', + * }); + */ +export const getStyles = ( + viewType: IterableEmbeddedViewType, + c?: IterableEmbeddedViewConfig | null +) => { + return { + backgroundColor: + c?.backgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.backgroundColor), + borderColor: + c?.borderColor ?? getDefaultStyle(viewType, embeddedStyles.borderColor), + borderWidth: + c?.borderWidth ?? getDefaultStyle(viewType, embeddedStyles.borderWidth), + borderCornerRadius: + c?.borderCornerRadius ?? + getDefaultStyle(viewType, embeddedStyles.borderCornerRadius), + primaryBtnBackgroundColor: + c?.primaryBtnBackgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.primaryBtnBackgroundColor), + primaryBtnTextColor: + c?.primaryBtnTextColor ?? + getDefaultStyle(viewType, embeddedStyles.primaryBtnTextColor), + secondaryBtnBackgroundColor: + c?.secondaryBtnBackgroundColor ?? + getDefaultStyle(viewType, embeddedStyles.secondaryBtnBackground), + secondaryBtnTextColor: + c?.secondaryBtnTextColor ?? + getDefaultStyle(viewType, embeddedStyles.secondaryBtnTextColor), + titleTextColor: + c?.titleTextColor ?? getDefaultStyle(viewType, embeddedStyles.titleText), + bodyTextColor: + c?.bodyTextColor ?? getDefaultStyle(viewType, embeddedStyles.bodyText), + }; +}; diff --git a/src/embedded/hooks/useEmbeddedView/index.ts b/src/embedded/hooks/useEmbeddedView/index.ts new file mode 100644 index 000000000..bf1a77d44 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/index.ts @@ -0,0 +1,2 @@ +export * from './useEmbeddedView'; +export { useEmbeddedView as default } from './useEmbeddedView'; diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts new file mode 100644 index 000000000..9c3f9201d --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -0,0 +1,42 @@ +import { useMemo } from 'react'; +import { IterableEmbeddedViewType } from '../../enums'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import { getStyles } from './getStyles'; + +/** + * This hook is used to manage the lifecycle of an embedded view. + * + * @param viewType - The type of view to render. + * @param props - The props for the embedded view. + * @returns The embedded view. + * + * @example + * const { parsedStyles } = useEmbeddedView(IterableEmbeddedViewType.Notification, { + * message, + * config, + * onButtonClick, + * onMessageClick, + * }); + * + * return ( + * + * {parsedStyles.backgroundColor} + * + * ); + */ +export const useEmbeddedView = ( + /** The type of view to render. */ + viewType: IterableEmbeddedViewType, + /** The props for the embedded view. */ + { + config, + }: IterableEmbeddedComponentProps +) => { + const parsedStyles = useMemo(() => { + return getStyles(viewType, config); + }, [viewType, config]); + + return { + parsedStyles, + }; +}; From c474b9368543973187c47cf3c75f319813dafd10 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:17:38 -0800 Subject: [PATCH 13/48] test: add unit tests for getStyles function in useEmbeddedView hook --- .../hooks/useEmbeddedView/getStyles.test.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/embedded/hooks/useEmbeddedView/getStyles.test.ts diff --git a/src/embedded/hooks/useEmbeddedView/getStyles.test.ts b/src/embedded/hooks/useEmbeddedView/getStyles.test.ts new file mode 100644 index 000000000..33d725cbe --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/getStyles.test.ts @@ -0,0 +1,156 @@ +import { getStyles } from './getStyles'; +import { IterableEmbeddedViewType } from '../../enums'; +import { + embeddedBackgroundColors, + embeddedBorderColors, + embeddedBorderRadius, + embeddedBorderWidth, + embeddedPrimaryBtnBackgroundColors, + embeddedPrimaryBtnTextColors, + embeddedSecondaryBtnBackgroundColors, + embeddedSecondaryBtnTextColors, + embeddedTitleTextColors, + embeddedBodyTextColors, +} from './embeddedViewDefaults'; + +describe('getStyles', () => { + describe('default styles by view type (no config)', () => { + it('returns Notification defaults when viewType is Notification', () => { + const result = getStyles(IterableEmbeddedViewType.Notification); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.notification); + expect(result.borderColor).toBe(embeddedBorderColors.notification); + expect(result.borderWidth).toBe(embeddedBorderWidth.notification); + expect(result.borderCornerRadius).toBe(embeddedBorderRadius.notification); + expect(result.primaryBtnBackgroundColor).toBe(embeddedPrimaryBtnBackgroundColors.notification); + expect(result.primaryBtnTextColor).toBe(embeddedPrimaryBtnTextColors.notification); + expect(result.secondaryBtnBackgroundColor).toBe(embeddedSecondaryBtnBackgroundColors.notification); + expect(result.secondaryBtnTextColor).toBe(embeddedSecondaryBtnTextColors.notification); + expect(result.titleTextColor).toBe(embeddedTitleTextColors.notification); + expect(result.bodyTextColor).toBe(embeddedBodyTextColors.notification); + }); + + it('returns Card defaults when viewType is Card', () => { + const result = getStyles(IterableEmbeddedViewType.Card); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.card); + expect(result.borderColor).toBe(embeddedBorderColors.card); + expect(result.borderWidth).toBe(embeddedBorderWidth.card); + expect(result.borderCornerRadius).toBe(embeddedBorderRadius.card); + expect(result.primaryBtnBackgroundColor).toBe(embeddedPrimaryBtnBackgroundColors.card); + expect(result.primaryBtnTextColor).toBe(embeddedPrimaryBtnTextColors.card); + expect(result.secondaryBtnBackgroundColor).toBe(embeddedSecondaryBtnBackgroundColors.card); + expect(result.secondaryBtnTextColor).toBe(embeddedSecondaryBtnTextColors.card); + expect(result.titleTextColor).toBe(embeddedTitleTextColors.card); + expect(result.bodyTextColor).toBe(embeddedBodyTextColors.card); + }); + + it('returns Banner defaults when viewType is Banner', () => { + const result = getStyles(IterableEmbeddedViewType.Banner); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.banner); + expect(result.borderColor).toBe(embeddedBorderColors.banner); + expect(result.borderWidth).toBe(embeddedBorderWidth.banner); + expect(result.borderCornerRadius).toBe(embeddedBorderRadius.banner); + expect(result.primaryBtnBackgroundColor).toBe(embeddedPrimaryBtnBackgroundColors.banner); + expect(result.primaryBtnTextColor).toBe(embeddedPrimaryBtnTextColors.banner); + expect(result.secondaryBtnBackgroundColor).toBe(embeddedSecondaryBtnBackgroundColors.banner); + expect(result.secondaryBtnTextColor).toBe(embeddedSecondaryBtnTextColors.banner); + expect(result.titleTextColor).toBe(embeddedTitleTextColors.banner); + expect(result.bodyTextColor).toBe(embeddedBodyTextColors.banner); + }); + + it('returns Banner defaults for unknown viewType (default branch)', () => { + const unknownViewType = 999 as IterableEmbeddedViewType; + const result = getStyles(unknownViewType); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.banner); + expect(result.primaryBtnBackgroundColor).toBe(embeddedPrimaryBtnBackgroundColors.banner); + }); + }); + + describe('with null or undefined config', () => { + it('returns defaults when config is null', () => { + const result = getStyles(IterableEmbeddedViewType.Notification, null); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.notification); + expect(result.primaryBtnTextColor).toBe(embeddedPrimaryBtnTextColors.notification); + }); + + it('returns defaults when config is undefined', () => { + const result = getStyles(IterableEmbeddedViewType.Card, undefined); + + expect(result.backgroundColor).toBe(embeddedBackgroundColors.card); + }); + }); + + describe('config overrides defaults', () => { + it('uses config values when provided, overrides all style keys', () => { + const config = { + backgroundColor: '#000000', + borderColor: '#111111', + borderWidth: 2, + borderCornerRadius: 10, + primaryBtnBackgroundColor: '#222222', + primaryBtnTextColor: '#333333', + secondaryBtnBackgroundColor: '#444444', + secondaryBtnTextColor: '#555555', + titleTextColor: '#666666', + bodyTextColor: '#777777', + }; + + const result = getStyles(IterableEmbeddedViewType.Notification, config); + + expect(result.backgroundColor).toBe('#000000'); + expect(result.borderColor).toBe('#111111'); + expect(result.borderWidth).toBe(2); + expect(result.borderCornerRadius).toBe(10); + expect(result.primaryBtnBackgroundColor).toBe('#222222'); + expect(result.primaryBtnTextColor).toBe('#333333'); + expect(result.secondaryBtnBackgroundColor).toBe('#444444'); + expect(result.secondaryBtnTextColor).toBe('#555555'); + expect(result.titleTextColor).toBe('#666666'); + expect(result.bodyTextColor).toBe('#777777'); + }); + + it('overrides only provided config keys, rest use view-type defaults', () => { + const config = { + backgroundColor: '#abc', + borderCornerRadius: 12, + }; + + const result = getStyles(IterableEmbeddedViewType.Card, config); + + expect(result.backgroundColor).toBe('#abc'); + expect(result.borderCornerRadius).toBe(12); + expect(result.borderColor).toBe(embeddedBorderColors.card); + expect(result.borderWidth).toBe(embeddedBorderWidth.card); + expect(result.primaryBtnBackgroundColor).toBe(embeddedPrimaryBtnBackgroundColors.card); + expect(result.primaryBtnTextColor).toBe(embeddedPrimaryBtnTextColors.card); + expect(result.secondaryBtnBackgroundColor).toBe(embeddedSecondaryBtnBackgroundColors.card); + expect(result.secondaryBtnTextColor).toBe(embeddedSecondaryBtnTextColors.card); + expect(result.titleTextColor).toBe(embeddedTitleTextColors.card); + expect(result.bodyTextColor).toBe(embeddedBodyTextColors.card); + }); + }); + + describe('return shape', () => { + it('returns an object with all expected style keys', () => { + const result = getStyles(IterableEmbeddedViewType.Banner); + + expect(result).toMatchObject({ + backgroundColor: expect.any(String), + borderColor: expect.any(String), + borderWidth: expect.any(Number), + borderCornerRadius: expect.any(Number), + primaryBtnBackgroundColor: expect.any(String), + primaryBtnTextColor: expect.any(String), + secondaryBtnBackgroundColor: expect.any(String), + secondaryBtnTextColor: expect.any(String), + titleTextColor: expect.any(String), + bodyTextColor: expect.any(String), + }); + expect(Object.keys(result)).toHaveLength(10); + }); + }); +}); From e0c0a24c7729d4de81ca9d006a44fd2c07e74a9e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:30:09 -0800 Subject: [PATCH 14/48] feat: add configuration modal to Embedded component for dynamic view settings --- .../components/Embedded/Embedded.styles.ts | 40 ++++++++++ example/src/components/Embedded/Embedded.tsx | 76 +++++++++++++++++++ .../components/IterableEmbeddedView.tsx | 39 +++++++--- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index a1e4f6257..c9a23adaa 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -15,6 +15,11 @@ import { const styles = StyleSheet.create({ button, buttonText, + configError: { + color: utilityColors.warning100, + fontSize: 12, + marginBottom: 8, + }, container, embeddedSection: { display: 'flex', @@ -26,6 +31,41 @@ const styles = StyleSheet.create({ inputContainer: { marginVertical: 10, }, + jsonEditor: { + ...input, + fontFamily: undefined, + fontSize: 12, + height: 220, + marginBottom: 12, + padding: 10, + }, + modalButton: { + flex: 1, + }, + modalButtons: { + flexDirection: 'row', + gap: 12, + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: colors.backgroundPrimary, + borderRadius: 12, + maxHeight: '80%', + padding: 16, + }, + // eslint-disable-next-line react-native/no-color-literals + modalOverlay: { + backgroundColor: 'rgba(0,0,0,0.5)', + flex: 1, + justifyContent: 'center', + padding: 20, + }, + modalTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + textAlign: 'center', + }, subtitle: { ...subtitle, textAlign: 'center' }, text: { textAlign: 'center' }, textInput: input, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 4ac18a6f4..4998319b6 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,4 +1,5 @@ import { + Modal, ScrollView, Text, TextInput, @@ -9,6 +10,7 @@ import { useCallback, useState } from 'react'; import { Iterable, type IterableEmbeddedMessage, + type IterableEmbeddedViewConfig, IterableEmbeddedView, IterableEmbeddedViewType, } from '@iterable/react-native-sdk'; @@ -16,6 +18,9 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './Embedded.styles'; +const DEFAULT_CONFIG_JSON = `{ +}`; + export const Embedded = () => { const [placementIdsInput, setPlacementIdsInput] = useState(''); const [embeddedMessages, setEmbeddedMessages] = useState< @@ -23,6 +28,11 @@ export const Embedded = () => { >([]); const [selectedViewType, setSelectedViewType] = useState(IterableEmbeddedViewType.Banner); + const [viewConfig, setViewConfig] = + useState(null); + const [configEditorVisible, setConfigEditorVisible] = useState(false); + const [configJson, setConfigJson] = useState(DEFAULT_CONFIG_JSON); + const [configError, setConfigError] = useState(null); // Parse placement IDs from input const parsedPlacementIds = placementIdsInput @@ -55,6 +65,30 @@ export const Embedded = () => { }); }, [idsToFetch]); + const openConfigEditor = useCallback(() => { + setConfigError(null); + setConfigJson( + viewConfig ? JSON.stringify(viewConfig, null, 2) : DEFAULT_CONFIG_JSON + ); + setConfigEditorVisible(true); + }, [viewConfig]); + + const applyConfig = useCallback(() => { + setConfigError(null); + try { + const parsed = JSON.parse(configJson) as IterableEmbeddedViewConfig; + setViewConfig(parsed); + setConfigEditorVisible(false); + } catch (e) { + setConfigError(e instanceof Error ? e.message : 'Invalid JSON'); + } + }, [configJson]); + + const closeConfigEditor = useCallback(() => { + setConfigEditorVisible(false); + setConfigError(null); + }, []); + return ( Embedded @@ -142,6 +176,9 @@ export const Embedded = () => { End session + + Set view config + Placement IDs (comma-separated): { + + + + View config (JSON) + {configError ? ( + {configError} + ) : null} + + + + Cancel + + + Apply + + + + + @@ -167,6 +242,7 @@ export const Embedded = () => { key={message.metadata.messageId} viewType={selectedViewType} message={message} + config={viewConfig} /> ))} diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index 91ec5e355..7234e3904 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -45,21 +45,40 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - const { parsedStyles } = - useEmbeddedView(IterableEmbeddedViewType.Notification, props); + const { parsedStyles } = useEmbeddedView(viewType, props); return Cmp ? ( - parsedStyles.backgroundColor: {String(parsedStyles.backgroundColor)} + + parsedStyles.backgroundColor: {String(parsedStyles.backgroundColor)} + parsedStyles.borderColor: {String(parsedStyles.borderColor)} parsedStyles.borderWidth: {parsedStyles.borderWidth} - parsedStyles.borderCornerRadius: {parsedStyles.borderCornerRadius} - parsedStyles.primaryBtnBackgroundColor: {String(parsedStyles.primaryBtnBackgroundColor)} - parsedStyles.primaryBtnTextColor: {String(parsedStyles.primaryBtnTextColor)} - parsedStyles.secondaryBtnBackgroundColor: {String(parsedStyles.secondaryBtnBackgroundColor)} - parsedStyles.secondaryBtnTextColor: {String(parsedStyles.secondaryBtnTextColor)} - parsedStyles.titleTextColor: {String(parsedStyles.titleTextColor)} - parsedStyles.bodyTextColor: {String(parsedStyles.bodyTextColor)} + + parsedStyles.borderCornerRadius: {parsedStyles.borderCornerRadius} + + + parsedStyles.primaryBtnBackgroundColor:{' '} + {String(parsedStyles.primaryBtnBackgroundColor)} + + + parsedStyles.primaryBtnTextColor:{' '} + {String(parsedStyles.primaryBtnTextColor)} + + + parsedStyles.secondaryBtnBackgroundColor:{' '} + {String(parsedStyles.secondaryBtnBackgroundColor)} + + + parsedStyles.secondaryBtnTextColor:{' '} + {String(parsedStyles.secondaryBtnTextColor)} + + + parsedStyles.titleTextColor: {String(parsedStyles.titleTextColor)} + + + parsedStyles.bodyTextColor: {String(parsedStyles.bodyTextColor)} + ) : null; From 05114b1e33f05cb4c1026b9d25b51e691dee7ee3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:40:06 -0800 Subject: [PATCH 15/48] refactor: extract modal styles into a separate file and update Embedded component styles --- .../components/Embedded/Embedded.styles.ts | 37 ++++------------ example/src/components/Embedded/Embedded.tsx | 1 - example/src/constants/styles/index.ts | 1 + example/src/constants/styles/modal.ts | 43 +++++++++++++++++++ 4 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 example/src/constants/styles/modal.ts diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index c9a23adaa..cbadf0fe2 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -7,9 +7,13 @@ import { container, hr, input, + modalButton, + modalButtons, + modalContent, + modalOverlay, subtitle, title, - utilityColors, + utilityColors } from '../../constants'; const styles = StyleSheet.create({ @@ -39,33 +43,10 @@ const styles = StyleSheet.create({ marginBottom: 12, padding: 10, }, - modalButton: { - flex: 1, - }, - modalButtons: { - flexDirection: 'row', - gap: 12, - justifyContent: 'flex-end', - }, - modalContent: { - backgroundColor: colors.backgroundPrimary, - borderRadius: 12, - maxHeight: '80%', - padding: 16, - }, - // eslint-disable-next-line react-native/no-color-literals - modalOverlay: { - backgroundColor: 'rgba(0,0,0,0.5)', - flex: 1, - justifyContent: 'center', - padding: 20, - }, - modalTitle: { - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - textAlign: 'center', - }, + modalButton, + modalButtons: modalButtons, + modalContent, + modalOverlay, subtitle: { ...subtitle, textAlign: 'center' }, text: { textAlign: 'center' }, textInput: input, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 4998319b6..a9cff8fbe 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -204,7 +204,6 @@ export const Embedded = () => { > - View config (JSON) {configError ? ( {configError} ) : null} diff --git a/example/src/constants/styles/index.ts b/example/src/constants/styles/index.ts index b8c3bac5e..225ee4903 100644 --- a/example/src/constants/styles/index.ts +++ b/example/src/constants/styles/index.ts @@ -2,5 +2,6 @@ export * from './colors'; export * from './containers'; export * from './formElements'; export * from './miscElements'; +export * from './modal'; export * from './shadows'; export * from './typography'; diff --git a/example/src/constants/styles/modal.ts b/example/src/constants/styles/modal.ts new file mode 100644 index 000000000..07f513a96 --- /dev/null +++ b/example/src/constants/styles/modal.ts @@ -0,0 +1,43 @@ +import type { TextStyle, ViewStyle } from "react-native"; +import { colors } from "./colors"; + +export const modalTitle: TextStyle = { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + textAlign: 'center', +}; + +export const modalOverlay: ViewStyle = { + backgroundColor: 'rgba(0,0,0,0.5)', + flex: 1, + justifyContent: 'center', + padding: 20, +}; + +export const modalContent: ViewStyle = { + backgroundColor: colors.backgroundPrimary, + borderRadius: 12, + maxHeight: '80%', + padding: 16, +}; + +export const modalButtons: ViewStyle = { + flexDirection: 'row', + gap: 12, + justifyContent: 'flex-end', +}; + +export const modalButton: ViewStyle = { + flex: 1, +}; + +export const modalButtonText: TextStyle = { + color: colors.brandCyan, + fontSize: 14, + fontWeight: '600', +}; + +export const modalButtonTextSelected: TextStyle = { + color: colors.backgroundPrimary, +}; From d09719080417f0fb85aa30b839e549776f8b6fab Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:46:18 -0800 Subject: [PATCH 16/48] refactor: simplify jsonEditor styles in Embedded component by removing unused properties --- example/src/components/Embedded/Embedded.styles.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index cbadf0fe2..fd9571252 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -37,11 +37,8 @@ const styles = StyleSheet.create({ }, jsonEditor: { ...input, - fontFamily: undefined, fontSize: 12, height: 220, - marginBottom: 12, - padding: 10, }, modalButton, modalButtons: modalButtons, From 7de5d61c4de9b0041848253f9628bdb289cd6c78 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 16:49:43 -0800 Subject: [PATCH 17/48] refactor: remove configError state, replacing error handling with an alert --- example/src/components/Embedded/Embedded.styles.ts | 5 ----- example/src/components/Embedded/Embedded.tsx | 13 +++---------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index fd9571252..9f416c3bb 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -19,11 +19,6 @@ import { const styles = StyleSheet.create({ button, buttonText, - configError: { - color: utilityColors.warning100, - fontSize: 12, - marginBottom: 8, - }, container, embeddedSection: { display: 'flex', diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index a9cff8fbe..599718377 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -1,4 +1,5 @@ import { + Alert, Modal, ScrollView, Text, @@ -32,7 +33,6 @@ export const Embedded = () => { useState(null); const [configEditorVisible, setConfigEditorVisible] = useState(false); const [configJson, setConfigJson] = useState(DEFAULT_CONFIG_JSON); - const [configError, setConfigError] = useState(null); // Parse placement IDs from input const parsedPlacementIds = placementIdsInput @@ -66,27 +66,23 @@ export const Embedded = () => { }, [idsToFetch]); const openConfigEditor = useCallback(() => { - setConfigError(null); setConfigJson( viewConfig ? JSON.stringify(viewConfig, null, 2) : DEFAULT_CONFIG_JSON ); - setConfigEditorVisible(true); }, [viewConfig]); const applyConfig = useCallback(() => { - setConfigError(null); try { const parsed = JSON.parse(configJson) as IterableEmbeddedViewConfig; setViewConfig(parsed); setConfigEditorVisible(false); - } catch (e) { - setConfigError(e instanceof Error ? e.message : 'Invalid JSON'); + } catch { + Alert.alert('Error', 'Invalid JSON'); } }, [configJson]); const closeConfigEditor = useCallback(() => { setConfigEditorVisible(false); - setConfigError(null); }, []); return ( @@ -204,9 +200,6 @@ export const Embedded = () => { > - {configError ? ( - {configError} - ) : null} Date: Mon, 23 Feb 2026 16:55:57 -0800 Subject: [PATCH 18/48] refactor: streamline modal button styles and ensure config editor visibility in Embedded component --- example/src/components/Embedded/Embedded.styles.ts | 2 +- example/src/components/Embedded/Embedded.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/components/Embedded/Embedded.styles.ts b/example/src/components/Embedded/Embedded.styles.ts index 9f416c3bb..a1fb26b7e 100644 --- a/example/src/components/Embedded/Embedded.styles.ts +++ b/example/src/components/Embedded/Embedded.styles.ts @@ -36,7 +36,7 @@ const styles = StyleSheet.create({ height: 220, }, modalButton, - modalButtons: modalButtons, + modalButtons, modalContent, modalOverlay, subtitle: { ...subtitle, textAlign: 'center' }, diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 599718377..189293d37 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -69,6 +69,7 @@ export const Embedded = () => { setConfigJson( viewConfig ? JSON.stringify(viewConfig, null, 2) : DEFAULT_CONFIG_JSON ); + setConfigEditorVisible(true); }, [viewConfig]); const applyConfig = useCallback(() => { From 2385cc0cc136bdbd398de36c9e681acf3f66dd81 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 17:00:13 -0800 Subject: [PATCH 19/48] fix: correct syntax in JSDoc example for useEmbeddedView hook --- src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index 9c3f9201d..eb4997782 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -11,16 +11,16 @@ import { getStyles } from './getStyles'; * @returns The embedded view. * * @example - * const { parsedStyles } = useEmbeddedView(IterableEmbeddedViewType.Notification, { + * const \{ parsedStyles \} = useEmbeddedView(IterableEmbeddedViewType.Notification, \{ * message, * config, * onButtonClick, * onMessageClick, - * }); + * \}); * * return ( * - * {parsedStyles.backgroundColor} + * \{parsedStyles.backgroundColor\} * * ); */ From 44ffd76bb623818f3723125f74ebd4873475b739 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 17:26:22 -0800 Subject: [PATCH 20/48] feat: enhance IterableEmbeddedView to display media content and add getMedia utility function --- .../components/IterableEmbeddedView.tsx | 38 +++---------------- .../hooks/useEmbeddedView/getMedia.ts | 31 +++++++++++++++ .../hooks/useEmbeddedView/useEmbeddedView.ts | 11 +++++- 3 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 src/embedded/hooks/useEmbeddedView/getMedia.ts diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index 7234e3904..365d6029d 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, Image } from 'react-native'; import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; @@ -45,40 +45,14 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - const { parsedStyles } = useEmbeddedView(viewType, props); + const { media } = useEmbeddedView(viewType, props); return Cmp ? ( - - parsedStyles.backgroundColor: {String(parsedStyles.backgroundColor)} - - parsedStyles.borderColor: {String(parsedStyles.borderColor)} - parsedStyles.borderWidth: {parsedStyles.borderWidth} - - parsedStyles.borderCornerRadius: {parsedStyles.borderCornerRadius} - - - parsedStyles.primaryBtnBackgroundColor:{' '} - {String(parsedStyles.primaryBtnBackgroundColor)} - - - parsedStyles.primaryBtnTextColor:{' '} - {String(parsedStyles.primaryBtnTextColor)} - - - parsedStyles.secondaryBtnBackgroundColor:{' '} - {String(parsedStyles.secondaryBtnBackgroundColor)} - - - parsedStyles.secondaryBtnTextColor:{' '} - {String(parsedStyles.secondaryBtnTextColor)} - - - parsedStyles.titleTextColor: {String(parsedStyles.titleTextColor)} - - - parsedStyles.bodyTextColor: {String(parsedStyles.bodyTextColor)} - + media.url: {media.url} + media.caption: {media.caption} + media.shouldShow: {media.shouldShow ? 'true' : 'false'} + {media.url ? : null} ) : null; diff --git a/src/embedded/hooks/useEmbeddedView/getMedia.ts b/src/embedded/hooks/useEmbeddedView/getMedia.ts new file mode 100644 index 000000000..7260ca407 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/getMedia.ts @@ -0,0 +1,31 @@ +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import { IterableEmbeddedViewType } from '../../enums'; + +/** + * This function is used to get the media to render for a given embedded view + * type and message. + * + * @param viewType - The type of view to render. + * @param message - The message to render. + * @returns The media to render. + * + * @example + * const media = getMedia(IterableEmbeddedViewType.Notification, message); + * console.log(media.url); + * console.log(media.caption); + * console.log(media.shouldShow ? 'true' : 'false'); + */ +export const getMedia = ( + /** The type of view to render. */ + viewType: IterableEmbeddedViewType, + /** The message to render. */ + message: IterableEmbeddedMessage +) => { + if (viewType === IterableEmbeddedViewType.Notification) { + return { url: null, caption: null, shouldShow: false }; + } + const url = message.elements?.mediaUrl ?? null; + const caption = message.elements?.mediaUrlCaption ?? null; + const shouldShow = !!url && url.length > 0; + return { url, caption, shouldShow }; +}; diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index eb4997782..1ed04d3ee 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -1,6 +1,8 @@ import { useMemo } from 'react'; + import { IterableEmbeddedViewType } from '../../enums'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import { getMedia } from './getMedia'; import { getStyles } from './getStyles'; /** @@ -11,7 +13,7 @@ import { getStyles } from './getStyles'; * @returns The embedded view. * * @example - * const \{ parsedStyles \} = useEmbeddedView(IterableEmbeddedViewType.Notification, \{ + * const \{ media, parsedStyles \} = useEmbeddedView(IterableEmbeddedViewType.Notification, \{ * message, * config, * onButtonClick, @@ -20,6 +22,8 @@ import { getStyles } from './getStyles'; * * return ( * + * \{media.url\} + * \{media.caption\} * \{parsedStyles.backgroundColor\} * * ); @@ -30,13 +34,18 @@ export const useEmbeddedView = ( /** The props for the embedded view. */ { config, + message, }: IterableEmbeddedComponentProps ) => { const parsedStyles = useMemo(() => { return getStyles(viewType, config); }, [viewType, config]); + const media = useMemo(() => { + return getMedia(viewType, message); + }, [viewType, message]); return { parsedStyles, + media, }; }; From d5c4c04fdeeb72a536a00fc4faf795406e12c77c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 17:44:30 -0800 Subject: [PATCH 21/48] test: add unit tests for getMedia function in useEmbeddedView hook --- .../hooks/useEmbeddedView/getMedia.test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/embedded/hooks/useEmbeddedView/getMedia.test.ts diff --git a/src/embedded/hooks/useEmbeddedView/getMedia.test.ts b/src/embedded/hooks/useEmbeddedView/getMedia.test.ts new file mode 100644 index 000000000..b4e793ffd --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/getMedia.test.ts @@ -0,0 +1,136 @@ +import { getMedia } from './getMedia'; +import { IterableEmbeddedViewType } from '../../enums'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; + +const minimalMessage: IterableEmbeddedMessage = { + metadata: { messageId: 'msg-1', placementId: 1 }, +}; + +describe('getMedia', () => { + describe('viewType Notification', () => { + it('returns no media regardless of message content', () => { + const result = getMedia(IterableEmbeddedViewType.Notification, minimalMessage); + + expect(result).toEqual({ url: null, caption: null, shouldShow: false }); + }); + + it('returns no media even when message has mediaUrl and caption', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { + mediaUrl: 'https://example.com/image.png', + mediaUrlCaption: 'Example caption', + }, + }; + + const result = getMedia(IterableEmbeddedViewType.Notification, message); + + expect(result).toEqual({ url: null, caption: null, shouldShow: false }); + }); + }); + + describe('viewType Card', () => { + it('returns url and caption from message.elements, shouldShow true when url is non-empty', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { + mediaUrl: 'https://example.com/photo.jpg', + mediaUrlCaption: 'A nice photo', + }, + }; + + const result = getMedia(IterableEmbeddedViewType.Card, message); + + expect(result).toEqual({ + url: 'https://example.com/photo.jpg', + caption: 'A nice photo', + shouldShow: true, + }); + }); + + it('returns url only (caption null) when message has no mediaUrlCaption', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { mediaUrl: 'https://example.com/img.png' }, + }; + + const result = getMedia(IterableEmbeddedViewType.Card, message); + + expect(result).toEqual({ + url: 'https://example.com/img.png', + caption: null, + shouldShow: true, + }); + }); + + it('returns shouldShow false when mediaUrl is empty string', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { mediaUrl: '', mediaUrlCaption: 'Caption' }, + }; + + const result = getMedia(IterableEmbeddedViewType.Card, message); + + expect(result.url).toBe(''); + expect(result.caption).toBe('Caption'); + expect(result.shouldShow).toBe(false); + }); + + it('returns null url/caption and shouldShow false when message has no elements', () => { + const result = getMedia(IterableEmbeddedViewType.Card, minimalMessage); + + expect(result).toEqual({ url: null, caption: null, shouldShow: false }); + }); + + it('returns null url/caption when elements exist but mediaUrl is undefined', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { title: 'Title', body: 'Body' }, + }; + + const result = getMedia(IterableEmbeddedViewType.Card, message); + + expect(result).toEqual({ url: null, caption: null, shouldShow: false }); + }); + }); + + describe('viewType Banner', () => { + it('returns url and caption from message.elements, shouldShow true when url is non-empty', () => { + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { + mediaUrl: 'https://example.com/banner.png', + mediaUrlCaption: 'Banner caption', + }, + }; + + const result = getMedia(IterableEmbeddedViewType.Banner, message); + + expect(result).toEqual({ + url: 'https://example.com/banner.png', + caption: 'Banner caption', + shouldShow: true, + }); + }); + + it('returns null url/caption and shouldShow false when message has no elements', () => { + const result = getMedia(IterableEmbeddedViewType.Banner, minimalMessage); + + expect(result).toEqual({ url: null, caption: null, shouldShow: false }); + }); + }); + + describe('return shape', () => { + it('returns an object with url, caption, and shouldShow', () => { + const result = getMedia(IterableEmbeddedViewType.Card, minimalMessage); + + expect(Object.keys(result)).toHaveLength(3); + expect(result).toHaveProperty('url'); + expect(result).toHaveProperty('caption'); + expect(result).toHaveProperty('shouldShow'); + expect(typeof result.shouldShow).toBe('boolean'); + expect(result.url === null || typeof result.url === 'string').toBe(true); + expect(result.caption === null || typeof result.caption === 'string').toBe(true); + }); + }); +}); From 8df31313c38bedf0edb28cdcc484aacd9441d7b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 18:10:49 -0800 Subject: [PATCH 22/48] test: add unit tests for IterableEmbeddedNotification component --- .../IterableEmbeddedNotification.test.tsx | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx new file mode 100644 index 000000000..0ea49c10d --- /dev/null +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx @@ -0,0 +1,347 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { fireEvent, render } from '@testing-library/react-native'; + +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; + +const mockHandleButtonClick = jest.fn(); +const mockHandleMessageClick = jest.fn(); + +jest.mock('../../hooks/useEmbeddedView', () => ({ + useEmbeddedView: jest.fn(), +})); + +const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< + typeof useEmbeddedView +>; + +const defaultParsedStyles = { + backgroundColor: '#ffffff', + borderColor: '#E0DEDF', + borderCornerRadius: 8, + borderWidth: 1, + primaryBtnBackgroundColor: '#6A266D', + primaryBtnTextColor: '#ffffff', + secondaryBtnBackgroundColor: 'transparent', + secondaryBtnTextColor: '#79347F', + titleTextColor: '#3D3A3B', + bodyTextColor: '#787174', +}; + +function mockUseEmbeddedViewReturn(overrides: Partial> = {}) { + mockUseEmbeddedView.mockReturnValue({ + parsedStyles: defaultParsedStyles, + handleButtonClick: mockHandleButtonClick, + handleMessageClick: mockHandleMessageClick, + media: { url: null, caption: null, shouldShow: false }, + ...overrides, + }); +} + +describe('IterableEmbeddedNotification', () => { + const baseMessage: IterableEmbeddedMessage = { + metadata: { + messageId: 'msg-1', + campaignId: 1, + placementId: 1, + }, + elements: { + title: 'Notification Title', + body: 'Notification body text.', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseEmbeddedViewReturn(); + }); + + describe('Rendering', () => { + it('should render without crashing', () => { + const { getByText } = render( + + ); + expect(getByText('Notification Title')).toBeTruthy(); + expect(getByText('Notification body text.')).toBeTruthy(); + }); + + it('should render title and body from message.elements', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + title: 'Custom Title', + body: 'Custom body content.', + }, + }; + const { getByText } = render( + + ); + expect(getByText('Custom Title')).toBeTruthy(); + expect(getByText('Custom body content.')).toBeTruthy(); + }); + + it('should apply parsedStyles to container and text', () => { + const customStyles = { + ...defaultParsedStyles, + backgroundColor: '#000000', + titleTextColor: '#ff0000', + bodyTextColor: '#00ff00', + }; + mockUseEmbeddedViewReturn({ parsedStyles: customStyles }); + + const { getByText, UNSAFE_getAllByType } = render( + + ); + + const views = UNSAFE_getAllByType('View' as any); + const styleArray = (s: any) => (Array.isArray(s) ? s : [s]); + const container = views.find( + (v: any) => + v.props.style && + styleArray(v.props.style).some( + (sty: any) => sty && sty.backgroundColor === '#000000' + ) + ); + expect(container).toBeTruthy(); + expect(styleArray(container!.props.style)).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + backgroundColor: '#000000', + borderColor: customStyles.borderColor, + borderRadius: customStyles.borderCornerRadius, + borderWidth: customStyles.borderWidth, + }), + ]) + ); + + const title = getByText('Notification Title'); + const body = getByText('Notification body text.'); + expect(title.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#ff0000' }), + ]) + ); + expect(body.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#00ff00' }), + ]) + ); + }); + + it('should not render button container when message has no buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: undefined }, + }; + const { queryByText } = render( + + ); + expect(queryByText('CTA')).toBeNull(); + }); + + it('should not render button container when buttons array is empty', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: [] }, + }; + const { queryByText } = render( + + ); + expect(queryByText('Primary')).toBeNull(); + }); + }); + + describe('Buttons', () => { + const primaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-primary', + title: 'Primary', + action: { type: 'openUrl', data: 'https://example.com' }, + }; + const secondaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-secondary', + title: 'Secondary', + }; + + it('should render buttons when message has buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + expect(getByText('Primary')).toBeTruthy(); + expect(getByText('Secondary')).toBeTruthy(); + }); + + it('should apply primary and secondary button text colors from parsedStyles', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + + const primaryText = getByText('Primary'); + const secondaryText = getByText('Secondary'); + expect(primaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.primaryBtnTextColor, + }), + ]) + ); + expect(secondaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.secondaryBtnTextColor, + }), + ]) + ); + }); + + it('should call handleButtonClick with correct button when button is pressed', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + + fireEvent.press(getByText('Primary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(1); + expect(mockHandleButtonClick).toHaveBeenCalledWith(primaryButton); + + fireEvent.press(getByText('Secondary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(2); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith(secondaryButton); + }); + }); + + describe('Message click', () => { + it('should call handleMessageClick when message area is pressed', () => { + const { getByText } = render( + + ); + + fireEvent.press(getByText('Notification Title')); + expect(mockHandleMessageClick).toHaveBeenCalledTimes(1); + + mockHandleMessageClick.mockClear(); + fireEvent.press(getByText('Notification body text.')); + expect(mockHandleMessageClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('useEmbeddedView integration', () => { + it('should call useEmbeddedView with Notification viewType and props', () => { + const config = { backgroundColor: '#abc' } as any; + const onButtonClick = jest.fn(); + const onMessageClick = jest.fn(); + + render( + + ); + + expect(mockUseEmbeddedView).toHaveBeenCalledTimes(1); + expect(mockUseEmbeddedView).toHaveBeenCalledWith( + IterableEmbeddedViewType.Notification, + { + message: baseMessage, + config, + onButtonClick, + onMessageClick, + } + ); + }); + + it('should call useEmbeddedView with default callbacks when not provided', () => { + render(); + + expect(mockUseEmbeddedView).toHaveBeenCalledWith( + IterableEmbeddedViewType.Notification, + expect.objectContaining({ + message: baseMessage, + onButtonClick: expect.any(Function), + onMessageClick: expect.any(Function), + }) + ); + }); + }); + + describe('Edge cases', () => { + it('should handle message with missing elements', () => { + const message: IterableEmbeddedMessage = { + metadata: baseMessage.metadata, + elements: undefined, + }; + const { queryByText } = render( + + ); + expect(queryByText('Notification Title')).toBeNull(); + expect(queryByText('Notification body text.')).toBeNull(); + }); + + it('should handle message with empty title and body without throwing', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { title: '', body: '' }, + }; + const { getAllByText } = render( + + ); + const emptyTextNodes = getAllByText(''); + expect(emptyTextNodes.length).toBeGreaterThanOrEqual(1); + }); + + it('should render multiple buttons and call handleButtonClick with correct button for each', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [ + { id: 'unique-id-1', title: 'First' }, + { id: 'unique-id-2', title: 'Second' }, + ], + }, + }; + const { getByText } = render( + + ); + expect(getByText('First')).toBeTruthy(); + expect(getByText('Second')).toBeTruthy(); + fireEvent.press(getByText('First')); + expect(mockHandleButtonClick).toHaveBeenCalledWith( + expect.objectContaining({ id: 'unique-id-1', title: 'First' }) + ); + fireEvent.press(getByText('Second')); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith( + expect.objectContaining({ id: 'unique-id-2', title: 'Second' }) + ); + }); + }); +}); From 47a283d117ad6366b3f816aad336bf442c2e5c94 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 23 Feb 2026 18:13:49 -0800 Subject: [PATCH 23/48] refactor: remove media display logic --- .../components/IterableEmbeddedView.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index 365d6029d..86844f15f 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -1,13 +1,10 @@ import { useMemo } from 'react'; -import { View, Text, Image } from 'react-native'; import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType'; - +import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; import { IterableEmbeddedCard } from './IterableEmbeddedCard'; import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; -import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; -import { useEmbeddedView } from '../hooks/useEmbeddedView/useEmbeddedView'; /** * The props for the IterableEmbeddedView component. @@ -45,15 +42,5 @@ export const IterableEmbeddedView = ({ } }, [viewType]); - const { media } = useEmbeddedView(viewType, props); - - return Cmp ? ( - - media.url: {media.url} - media.caption: {media.caption} - media.shouldShow: {media.shouldShow ? 'true' : 'false'} - {media.url ? : null} - - - ) : null; + return Cmp ? : null; }; From 7278e0cf58dfdb775c0bda55e393a02dfb22b8d9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 24 Feb 2026 13:50:53 -0800 Subject: [PATCH 24/48] test: add unit tests for useEmbeddedView hook functionality --- .../useEmbeddedView/useEmbeddedView.test.ts | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts new file mode 100644 index 000000000..4fd06e6c2 --- /dev/null +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts @@ -0,0 +1,234 @@ +import { renderHook, act } from '@testing-library/react-native'; + +import { Iterable } from '../../../core/classes/Iterable'; +import { IterableAction } from '../../../core/classes/IterableAction'; +import { IterableEmbeddedViewType } from '../../enums'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton'; +import { useEmbeddedView } from './useEmbeddedView'; +import { getMedia } from './getMedia'; +import { getStyles } from './getStyles'; + +jest.mock('./getMedia'); +jest.mock('./getStyles'); + +const mockGetMedia = getMedia as jest.MockedFunction; +const mockGetStyles = getStyles as jest.MockedFunction; + +const minimalMessage: IterableEmbeddedMessage = { + metadata: { messageId: 'msg-1', placementId: 1 }, +}; + +const defaultMedia = { url: null, caption: null, shouldShow: false }; +const defaultStyles = { + backgroundColor: '#ffffff', + borderColor: '#E0DEDF', + borderWidth: 1, + borderCornerRadius: 10, + primaryBtnBackgroundColor: '#6A266D', + primaryBtnTextColor: '#ffffff', + secondaryBtnBackgroundColor: 'transparent', + secondaryBtnTextColor: '#ffffff', + titleTextColor: '#000000', + bodyTextColor: '#000000', +}; + +describe('useEmbeddedView', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetMedia.mockReturnValue(defaultMedia); + mockGetStyles.mockReturnValue(defaultStyles); + jest.spyOn(Iterable.embeddedManager, 'handleClick').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('return shape', () => { + it('returns handleButtonClick, handleMessageClick, media, and parsedStyles', () => { + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { message: minimalMessage }) + ); + + expect(result.current).toHaveProperty('handleButtonClick'); + expect(result.current).toHaveProperty('handleMessageClick'); + expect(result.current).toHaveProperty('media'); + expect(result.current).toHaveProperty('parsedStyles'); + expect(typeof result.current.handleButtonClick).toBe('function'); + expect(typeof result.current.handleMessageClick).toBe('function'); + }); + }); + + describe('getMedia / getStyles delegation', () => { + it('calls getMedia with viewType and message', () => { + renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Card, { message: minimalMessage }) + ); + + expect(mockGetMedia).toHaveBeenCalledWith(IterableEmbeddedViewType.Card, minimalMessage); + }); + + it('calls getStyles with viewType and config', () => { + const config = { backgroundColor: '#000000' }; + renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { + message: minimalMessage, + config, + }) + ); + + expect(mockGetStyles).toHaveBeenCalledWith(IterableEmbeddedViewType.Notification, config); + }); + + it('returns media from getMedia and parsedStyles from getStyles', () => { + const customMedia = { url: 'https://example.com/img.png', caption: 'Cap', shouldShow: true }; + const customStyles = { ...defaultStyles, backgroundColor: '#111111' }; + mockGetMedia.mockReturnValue(customMedia); + mockGetStyles.mockReturnValue(customStyles); + + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Banner, { message: minimalMessage }) + ); + + expect(result.current.media).toEqual(customMedia); + expect(result.current.parsedStyles).toEqual(customStyles); + }); + }); + + describe('handleButtonClick', () => { + it('calls onButtonClick with the button', () => { + const onButtonClick = jest.fn(); + const button: IterableEmbeddedMessageElementsButton = { + id: 'btn-1', + title: 'Click me', + action: null, + }; + + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { + message: minimalMessage, + onButtonClick, + }) + ); + + act(() => { + result.current.handleButtonClick(button); + }); + + expect(onButtonClick).toHaveBeenCalledTimes(1); + expect(onButtonClick).toHaveBeenCalledWith(button); + }); + + it('calls Iterable.embeddedManager.handleClick with message, button.id, and button.action', () => { + const action = new IterableAction('openUrl', 'https://example.com'); + const button: IterableEmbeddedMessageElementsButton = { + id: 'btn-2', + title: 'Link', + action, + }; + + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { + message: minimalMessage, + }) + ); + + act(() => { + result.current.handleButtonClick(button); + }); + + expect(Iterable.embeddedManager.handleClick).toHaveBeenCalledWith( + minimalMessage, + 'btn-2', + action + ); + }); + }); + + describe('handleMessageClick', () => { + it('calls onMessageClick', () => { + const onMessageClick = jest.fn(); + + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { + message: minimalMessage, + onMessageClick, + }) + ); + + act(() => { + result.current.handleMessageClick(); + }); + + expect(onMessageClick).toHaveBeenCalledTimes(1); + }); + + it('calls Iterable.embeddedManager.handleClick with message, null, and message.elements?.defaultAction', () => { + const defaultAction = new IterableAction('openUrl', 'https://iterable.com'); + const message: IterableEmbeddedMessage = { + ...minimalMessage, + elements: { defaultAction }, + }; + + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { message }) + ); + + act(() => { + result.current.handleMessageClick(); + }); + + expect(Iterable.embeddedManager.handleClick).toHaveBeenCalledWith( + message, + null, + defaultAction + ); + }); + + it('calls embeddedManager.handleClick with undefined defaultAction when message has no elements', () => { + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { message: minimalMessage }) + ); + + act(() => { + result.current.handleMessageClick(); + }); + + expect(Iterable.embeddedManager.handleClick).toHaveBeenCalledWith( + minimalMessage, + null, + undefined + ); + }); + }); + + describe('default callbacks', () => { + it('does not throw when handleButtonClick is invoked without provided onButtonClick', () => { + const button: IterableEmbeddedMessageElementsButton = { id: 'b', title: null, action: null }; + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { message: minimalMessage }) + ); + + expect(() => { + act(() => { + result.current.handleButtonClick(button); + }); + }).not.toThrow(); + }); + + it('does not throw when handleMessageClick is invoked without provided onMessageClick', () => { + const { result } = renderHook(() => + useEmbeddedView(IterableEmbeddedViewType.Notification, { message: minimalMessage }) + ); + + expect(() => { + act(() => { + result.current.handleMessageClick(); + }); + }).not.toThrow(); + }); + }); + + // memoization behavior (useMemo) is indirectly exercised above via getMedia/getStyles calls +}); From a22a03985484a35d13e6ecf1233e9399fe571878 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 24 Feb 2026 14:11:15 -0800 Subject: [PATCH 25/48] refactor: replace IterableEmbeddedBanner component with a new implementation and add styles --- .../components/IterableEmbeddedBanner.tsx | 19 --- .../IterableEmbeddedBanner.styles.ts | 82 +++++++++++ .../IterableEmbeddedBanner.tsx | 130 ++++++++++++++++++ .../IterableEmbeddedBanner/index.ts | 2 + 4 files changed, 214 insertions(+), 19 deletions(-) delete mode 100644 src/embedded/components/IterableEmbeddedBanner.tsx create mode 100644 src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts create mode 100644 src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx create mode 100644 src/embedded/components/IterableEmbeddedBanner/index.ts diff --git a/src/embedded/components/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner.tsx deleted file mode 100644 index 56b4ca32b..000000000 --- a/src/embedded/components/IterableEmbeddedBanner.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { View, Text } from 'react-native'; - -import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; - -export const IterableEmbeddedBanner = ({ - config, - message, - onButtonClick = () => {}, -}: IterableEmbeddedComponentProps) => { - console.log(`🚀 > IterableEmbeddedBanner > config:`, config); - console.log(`🚀 > IterableEmbeddedBanner > message:`, message); - console.log(`🚀 > IterableEmbeddedBanner > onButtonClick:`, onButtonClick); - - return ( - - IterableEmbeddedBanner - - ); -}; diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts new file mode 100644 index 000000000..89c159f76 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts @@ -0,0 +1,82 @@ +import { StyleSheet, Platform } from 'react-native'; + +// See https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages#banners +export const IMAGE_HEIGHT = Platform.OS === 'android' ? 80 : 100; +export const IMAGE_WIDTH = Platform.OS === 'android' ? 80 : 100; + +export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + alignItems: 'center', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + paddingTop: 4, + }, + button: { + borderRadius: 32, + gap: 8, + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + paddingHorizontal: 12, + paddingVertical: 8, + }, + container: { + alignItems: 'flex-start', + borderStyle: 'solid', + boxShadow: + '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + gap: 16, + justifyContent: 'center', + padding: 16, + width: '100%', + }, + mediaContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + }, + mediaImage: { + borderRadius: 6, + borderStyle: 'solid', + borderWidth: 1, + height: IMAGE_HEIGHT, + paddingHorizontal: 0, + paddingVertical: 0, + width: IMAGE_WIDTH, + }, + textContainer: { + alignSelf: 'center', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + gap: 4, + width: '100%', + }, + title: { + fontSize: 16, + fontWeight: '700', + lineHeight: 16, + paddingBottom: 4, + }, +}); diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx new file mode 100644 index 000000000..c5892e04c --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -0,0 +1,130 @@ +import { + Image, + 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, +} from './IterableEmbeddedBanner.styles'; + +/** + * TODO: figure out how default action works. + */ + +export const IterableEmbeddedBanner = ({ + config, + message, + onButtonClick = () => {}, + onMessageClick = () => {}, +}: IterableEmbeddedComponentProps) => { + const { parsedStyles, media, handleButtonClick, handleMessageClick } = + useEmbeddedView(IterableEmbeddedViewType.Banner, { + message, + config, + onButtonClick, + onMessageClick, + }); + + const buttons = message.elements?.buttons ?? []; + + return ( + handleMessageClick()}> + + {} + + + + {message.elements?.title} + + + {message.elements?.body} + + + {media.shouldShow && ( + + {media.caption + + )} + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + handleButtonClick(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} + + + ); +}; diff --git a/src/embedded/components/IterableEmbeddedBanner/index.ts b/src/embedded/components/IterableEmbeddedBanner/index.ts new file mode 100644 index 000000000..39ed47189 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/index.ts @@ -0,0 +1,2 @@ +export * from './IterableEmbeddedBanner'; +export { IterableEmbeddedBanner as default } from './IterableEmbeddedBanner'; From 2f0666147c09d5b364387b6e457c10a2012fb6f7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 24 Feb 2026 14:39:03 -0800 Subject: [PATCH 26/48] test: add unit tests for IterableEmbeddedBanner component functionality --- .../IterableEmbeddedBanner.test.tsx | 372 ++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx new file mode 100644 index 000000000..61657be68 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx @@ -0,0 +1,372 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { fireEvent, render } from '@testing-library/react-native'; + +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedBanner } from './IterableEmbeddedBanner'; + +const mockHandleButtonClick = jest.fn(); +const mockHandleMessageClick = jest.fn(); + +jest.mock('../../hooks/useEmbeddedView', () => ({ + useEmbeddedView: jest.fn(), +})); + +const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< + typeof useEmbeddedView +>; + +const defaultParsedStyles = { + backgroundColor: '#ffffff', + borderColor: '#E0DEDF', + borderCornerRadius: 8, + borderWidth: 1, + primaryBtnBackgroundColor: '#6A266D', + primaryBtnTextColor: '#ffffff', + secondaryBtnBackgroundColor: 'transparent', + secondaryBtnTextColor: '#79347F', + titleTextColor: '#3D3A3B', + bodyTextColor: '#787174', +}; + +function mockUseEmbeddedViewReturn( + overrides: Partial> = {} +) { + mockUseEmbeddedView.mockReturnValue({ + parsedStyles: defaultParsedStyles, + handleButtonClick: mockHandleButtonClick, + handleMessageClick: mockHandleMessageClick, + media: { url: null, caption: null, shouldShow: false }, + ...overrides, + }); +} + +describe('IterableEmbeddedBanner', () => { + const baseMessage: IterableEmbeddedMessage = { + metadata: { + messageId: 'msg-1', + campaignId: 1, + placementId: 1, + }, + elements: { + title: 'Banner Title', + body: 'Banner body text.', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseEmbeddedViewReturn(); + }); + + describe('Rendering', () => { + it('should render without crashing', () => { + const { getByText } = render( + + ); + expect(getByText('Banner Title')).toBeTruthy(); + expect(getByText('Banner body text.')).toBeTruthy(); + }); + + it('should render title and body from message.elements', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + title: 'Custom Banner Title', + body: 'Custom banner body.', + }, + }; + const { getByText } = render( + + ); + expect(getByText('Custom Banner Title')).toBeTruthy(); + expect(getByText('Custom banner body.')).toBeTruthy(); + }); + + it('should apply parsedStyles to container and text', () => { + const customStyles = { + ...defaultParsedStyles, + backgroundColor: '#000000', + titleTextColor: '#ff0000', + bodyTextColor: '#00ff00', + }; + mockUseEmbeddedViewReturn({ parsedStyles: customStyles }); + + const { getByText, UNSAFE_getAllByType } = render( + + ); + + const views = UNSAFE_getAllByType('View' as any); + const styleArray = (s: any) => (Array.isArray(s) ? s : [s]); + const container = views.find( + (v: any) => + v.props.style && + styleArray(v.props.style).some( + (sty: any) => sty && sty.backgroundColor === '#000000' + ) + ); + expect(container).toBeTruthy(); + expect(styleArray(container!.props.style)).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + backgroundColor: '#000000', + borderColor: customStyles.borderColor, + borderRadius: customStyles.borderCornerRadius, + borderWidth: customStyles.borderWidth, + }), + ]) + ); + + const title = getByText('Banner Title'); + const body = getByText('Banner body text.'); + expect(title.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#ff0000' }), + ]) + ); + expect(body.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#00ff00' }), + ]) + ); + }); + + it('should not render button container when message has no buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: undefined }, + }; + const { queryByText } = render( + + ); + expect(queryByText('Primary')).toBeNull(); + }); + + it('should not render button container when buttons array is empty', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: [] }, + }; + const { queryByText } = render( + + ); + expect(queryByText('Primary')).toBeNull(); + }); + }); + + describe('Buttons', () => { + const primaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-primary', + title: 'Primary', + action: { type: 'openUrl', data: 'https://example.com' }, + }; + const secondaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-secondary', + title: 'Secondary', + }; + + it('should render buttons when message has buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + expect(getByText('Primary')).toBeTruthy(); + expect(getByText('Secondary')).toBeTruthy(); + }); + + it('should apply primary and secondary button text colors from parsedStyles', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + + const primaryText = getByText('Primary'); + const secondaryText = getByText('Secondary'); + expect(primaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.primaryBtnTextColor, + }), + ]) + ); + expect(secondaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.secondaryBtnTextColor, + }), + ]) + ); + }); + + it('should call handleButtonClick with correct button when button is pressed', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render( + + ); + + fireEvent.press(getByText('Primary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(1); + expect(mockHandleButtonClick).toHaveBeenCalledWith(primaryButton); + + fireEvent.press(getByText('Secondary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(2); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith(secondaryButton); + }); + }); + + describe('Media', () => { + it('should not render media when media.shouldShow is false', () => { + const { UNSAFE_queryAllByType } = render( + + ); + const images = UNSAFE_queryAllByType('Image' as any); + expect(images.length).toBe(0); + }); + + it('should render media image when media.shouldShow is true', () => { + const media = { + url: 'https://example.com/image.png', + caption: 'Banner image', + shouldShow: true, + }; + mockUseEmbeddedViewReturn({ media }); + + const { UNSAFE_queryAllByType } = render( + + ); + + const images = UNSAFE_queryAllByType('Image' as any); + expect(images.length).toBeGreaterThan(0); + expect((images[0] as any).props.source.uri).toBe(media.url); + }); + }); + + describe('Message click', () => { + it('should call handleMessageClick when banner is pressed', () => { + const { getByText } = render( + + ); + + fireEvent.press(getByText('Banner Title')); + expect(mockHandleMessageClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('useEmbeddedView integration', () => { + it('should call useEmbeddedView with Banner viewType and props', () => { + const config = { backgroundColor: '#abc' } as any; + const onButtonClick = jest.fn(); + const onMessageClick = jest.fn(); + + render( + + ); + + expect(mockUseEmbeddedView).toHaveBeenCalledTimes(1); + expect(mockUseEmbeddedView).toHaveBeenCalledWith( + IterableEmbeddedViewType.Banner, + { + message: baseMessage, + config, + onButtonClick, + onMessageClick, + } + ); + }); + + it('should call useEmbeddedView with default callbacks when not provided', () => { + render(); + + expect(mockUseEmbeddedView).toHaveBeenCalledWith( + IterableEmbeddedViewType.Banner, + expect.objectContaining({ + message: baseMessage, + onButtonClick: expect.any(Function), + onMessageClick: expect.any(Function), + }) + ); + }); + }); + + describe('Edge cases', () => { + it('should handle message with missing elements', () => { + const message: IterableEmbeddedMessage = { + metadata: baseMessage.metadata, + elements: undefined, + }; + const { queryByText } = render( + + ); + expect(queryByText('Banner Title')).toBeNull(); + expect(queryByText('Banner body text.')).toBeNull(); + }); + + it('should handle message with empty title and body without throwing', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { title: '', body: '' }, + }; + const { getAllByText } = render( + + ); + const emptyTextNodes = getAllByText(''); + expect(emptyTextNodes.length).toBeGreaterThanOrEqual(1); + }); + + it('should render multiple buttons and call handleButtonClick with correct button for each', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [ + { id: 'unique-id-1', title: 'First' }, + { id: 'unique-id-2', title: 'Second' }, + ], + }, + }; + const { getByText } = render( + + ); + expect(getByText('First')).toBeTruthy(); + expect(getByText('Second')).toBeTruthy(); + fireEvent.press(getByText('First')); + expect(mockHandleButtonClick).toHaveBeenCalledWith( + expect.objectContaining({ id: 'unique-id-1', title: 'First' }) + ); + fireEvent.press(getByText('Second')); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith( + expect.objectContaining({ id: 'unique-id-2', title: 'Second' }) + ); + }); + }); +}); From b6d5354ad8c706337c7aabf97b0a47f6872b1246 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 27 Feb 2026 13:28:47 -0800 Subject: [PATCH 27/48] feat: add embedded view default styles and update imports in getStyles files --- .../useEmbeddedView => constants}/embeddedViewDefaults.ts | 0 src/embedded/hooks/useEmbeddedView/getStyles.test.ts | 2 +- src/embedded/hooks/useEmbeddedView/getStyles.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/embedded/{hooks/useEmbeddedView => constants}/embeddedViewDefaults.ts (100%) diff --git a/src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts similarity index 100% rename from src/embedded/hooks/useEmbeddedView/embeddedViewDefaults.ts rename to src/embedded/constants/embeddedViewDefaults.ts diff --git a/src/embedded/hooks/useEmbeddedView/getStyles.test.ts b/src/embedded/hooks/useEmbeddedView/getStyles.test.ts index 33d725cbe..19c6092b6 100644 --- a/src/embedded/hooks/useEmbeddedView/getStyles.test.ts +++ b/src/embedded/hooks/useEmbeddedView/getStyles.test.ts @@ -11,7 +11,7 @@ import { embeddedSecondaryBtnTextColors, embeddedTitleTextColors, embeddedBodyTextColors, -} from './embeddedViewDefaults'; +} from '../../constants/embeddedViewDefaults'; describe('getStyles', () => { describe('default styles by view type (no config)', () => { diff --git a/src/embedded/hooks/useEmbeddedView/getStyles.ts b/src/embedded/hooks/useEmbeddedView/getStyles.ts index 16aa2f616..de2c69206 100644 --- a/src/embedded/hooks/useEmbeddedView/getStyles.ts +++ b/src/embedded/hooks/useEmbeddedView/getStyles.ts @@ -1,5 +1,5 @@ import type { IterableEmbeddedViewConfig } from '../../types/IterableEmbeddedViewConfig'; -import { embeddedStyles } from './embeddedViewDefaults'; +import { embeddedStyles } from '../../constants/embeddedViewDefaults'; import { IterableEmbeddedViewType } from '../../enums'; /** From fd91c2b1a9a1a6039334220a2e6cf100e8437cd0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 27 Feb 2026 13:31:05 -0800 Subject: [PATCH 28/48] refactor: remove unused media image border and background color constants --- src/embedded/constants/embeddedViewDefaults.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/embedded/constants/embeddedViewDefaults.ts b/src/embedded/constants/embeddedViewDefaults.ts index f20879388..10fa85bf7 100644 --- a/src/embedded/constants/embeddedViewDefaults.ts +++ b/src/embedded/constants/embeddedViewDefaults.ts @@ -58,25 +58,12 @@ export const embeddedBorderWidth = { banner: 1, }; -export const embeddedMediaImageBorderColors = { - notification: '#E0DEDF', - card: '#E0DEDF', - banner: '#E0DEDF', -}; - -export const embeddedMediaImageBackgroundColors = { - notification: '#F5F4F4', - card: '#F5F4F4', - banner: '#F5F4F4', -}; - export const embeddedStyles = { backgroundColor: embeddedBackgroundColors, bodyText: embeddedBodyTextColors, borderColor: embeddedBorderColors, borderCornerRadius: embeddedBorderRadius, borderWidth: embeddedBorderWidth, - mediaImageBorder: embeddedMediaImageBorderColors, primaryBtnBackgroundColor: embeddedPrimaryBtnBackgroundColors, primaryBtnTextColor: embeddedPrimaryBtnTextColors, secondaryBtnBackground: embeddedSecondaryBtnBackgroundColors, From 82f9b156a4e035bc986ba408e9570211cb68fdb4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 13 Mar 2026 10:29:05 -0700 Subject: [PATCH 29/48] feat: update getMedia function with return type --- src/embedded/hooks/useEmbeddedView/getMedia.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/embedded/hooks/useEmbeddedView/getMedia.ts b/src/embedded/hooks/useEmbeddedView/getMedia.ts index 7260ca407..ecc63ea3b 100644 --- a/src/embedded/hooks/useEmbeddedView/getMedia.ts +++ b/src/embedded/hooks/useEmbeddedView/getMedia.ts @@ -20,7 +20,14 @@ export const getMedia = ( viewType: IterableEmbeddedViewType, /** The message to render. */ message: IterableEmbeddedMessage -) => { +): { + /** The URL of the media to render. */ + url: string | null; + /** The caption of the media to render. */ + caption: string | null; + /** Whether the media should be shown. */ + shouldShow: boolean; +} => { if (viewType === IterableEmbeddedViewType.Notification) { return { url: null, caption: null, shouldShow: false }; } From 82b2cda9e81c760406afb08ab3e50e00562f143f Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 13 Mar 2026 11:10:38 -0700 Subject: [PATCH 30/48] feat: enhance IterableEmbeddedNotification styles with shadow properties and elevation --- .../IterableEmbeddedNotification.styles.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts index 923df66fc..01df507a5 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts @@ -1,5 +1,7 @@ import { StyleSheet } from 'react-native'; +const SHADOW_COLOR = 'rgba(0, 0, 0, 0.06)'; + export const styles = StyleSheet.create({ body: { alignSelf: 'stretch', @@ -38,12 +40,17 @@ export const styles = StyleSheet.create({ alignItems: 'flex-start', borderStyle: 'solid', boxShadow: - '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + `0 1px 1px 0 ${SHADOW_COLOR}, 0 0 2px 0 ${SHADOW_COLOR}, 0 0 1px 0 ${SHADOW_COLOR}`, display: 'flex', + elevation: 1, flexDirection: 'column', gap: 8, justifyContent: 'center', padding: 16, + shadowColor: SHADOW_COLOR, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.9, + shadowRadius: 2, width: '100%', }, title: { From 028323f4e60f3cbc8097033ee86e0bbbddde5c23 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 13 Mar 2026 11:34:26 -0700 Subject: [PATCH 31/48] refactor: update default props for onButtonClick and onMessageClick in useEmbeddedView --- .../IterableEmbeddedNotification.tsx | 5 ++--- src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index f0909cfc5..856ef4da9 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -15,8 +15,8 @@ import { styles } from './IterableEmbeddedNotification.styles'; export const IterableEmbeddedNotification = ({ config, message, - onButtonClick = () => {}, - onMessageClick = () => {}, + onButtonClick, + onMessageClick, }: IterableEmbeddedComponentProps) => { const { parsedStyles, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Notification, { @@ -41,7 +41,6 @@ export const IterableEmbeddedNotification = ({ } as ViewStyle, ]} > - {} {}; + /** * This hook is used to manage the lifecycle of an embedded view. * @@ -36,8 +38,8 @@ export const useEmbeddedView = ( { message, config, - onButtonClick = () => {}, - onMessageClick = () => {}, + onButtonClick = noop, + onMessageClick = noop, }: IterableEmbeddedComponentProps ) => { const parsedStyles = useMemo(() => { From 9b76334ed52028ea970477d0f3a1d75dc4e3e9f9 Mon Sep 17 00:00:00 2001 From: Loren <3190869+lposen@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:34:43 -0700 Subject: [PATCH 32/48] Apply suggestion from @joaodordio Co-authored-by: Joao Dordio --- src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index bd360efa3..7dcd663da 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -49,7 +49,6 @@ export const useEmbeddedView = ( return getMedia(viewType, message); }, [viewType, message]); - const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { onButtonClick(button); From 573db46764338290a0ec7b71dcfec671c275dc4d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 17 Mar 2026 11:27:46 -0700 Subject: [PATCH 33/48] style: enhance IterableEmbeddedBanner with shadow properties and elevation --- .../IterableEmbeddedBanner.styles.ts | 9 ++++++++- .../IterableEmbeddedBanner/IterableEmbeddedBanner.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts index 89c159f76..12c6a1d50 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.styles.ts @@ -3,6 +3,8 @@ import { StyleSheet, Platform } from 'react-native'; // See https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages#banners export const IMAGE_HEIGHT = Platform.OS === 'android' ? 80 : 100; export const IMAGE_WIDTH = Platform.OS === 'android' ? 80 : 100; +const SHADOW_COLOR_LIGHT = 'rgba(0, 0, 0, 0.06)'; +const SHADOW_COLOR_DARK = 'rgba(0, 0, 0, 0.08)'; export const styles = StyleSheet.create({ body: { @@ -41,12 +43,17 @@ export const styles = StyleSheet.create({ alignItems: 'flex-start', borderStyle: 'solid', boxShadow: - '0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)', + `0 1px 1px 0 ${SHADOW_COLOR_LIGHT}, 0 0 2px 0 ${SHADOW_COLOR_LIGHT}, 0 0 1px 0 ${SHADOW_COLOR_DARK}`, display: 'flex', + elevation: 1, flexDirection: 'column', gap: 16, justifyContent: 'center', padding: 16, + shadowColor: SHADOW_COLOR_LIGHT, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.9, + shadowRadius: 2, width: '100%', }, mediaContainer: { diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index c5892e04c..7534c8bb6 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -18,6 +18,8 @@ import { IMAGE_WIDTH, } from './IterableEmbeddedBanner.styles'; +const noop = () => {}; + /** * TODO: figure out how default action works. */ @@ -25,8 +27,8 @@ import { export const IterableEmbeddedBanner = ({ config, message, - onButtonClick = () => {}, - onMessageClick = () => {}, + onButtonClick = noop, + onMessageClick = noop, }: IterableEmbeddedComponentProps) => { const { parsedStyles, media, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Banner, { @@ -51,7 +53,6 @@ export const IterableEmbeddedBanner = ({ } as ViewStyle, ]} > - {} Date: Tue, 17 Mar 2026 11:42:36 -0700 Subject: [PATCH 34/48] refactor: remove redundant default callbacks and failing test --- .../IterableEmbeddedBanner.test.tsx | 13 ------------- .../IterableEmbeddedBanner.tsx | 6 ++---- .../IterableEmbeddedNotification.test.tsx | 13 ------------- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx index 61657be68..aab2230fa 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.test.tsx @@ -303,19 +303,6 @@ describe('IterableEmbeddedBanner', () => { } ); }); - - it('should call useEmbeddedView with default callbacks when not provided', () => { - render(); - - expect(mockUseEmbeddedView).toHaveBeenCalledWith( - IterableEmbeddedViewType.Banner, - expect.objectContaining({ - message: baseMessage, - onButtonClick: expect.any(Function), - onMessageClick: expect.any(Function), - }) - ); - }); }); describe('Edge cases', () => { diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 7534c8bb6..5deaa17d0 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -18,8 +18,6 @@ import { IMAGE_WIDTH, } from './IterableEmbeddedBanner.styles'; -const noop = () => {}; - /** * TODO: figure out how default action works. */ @@ -27,8 +25,8 @@ const noop = () => {}; export const IterableEmbeddedBanner = ({ config, message, - onButtonClick = noop, - onMessageClick = noop, + onButtonClick, + onMessageClick, }: IterableEmbeddedComponentProps) => { const { parsedStyles, media, handleButtonClick, handleMessageClick } = useEmbeddedView(IterableEmbeddedViewType.Banner, { diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx index 0ea49c10d..3619cfc4f 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.test.tsx @@ -278,19 +278,6 @@ describe('IterableEmbeddedNotification', () => { } ); }); - - it('should call useEmbeddedView with default callbacks when not provided', () => { - render(); - - expect(mockUseEmbeddedView).toHaveBeenCalledWith( - IterableEmbeddedViewType.Notification, - expect.objectContaining({ - message: baseMessage, - onButtonClick: expect.any(Function), - onMessageClick: expect.any(Function), - }) - ); - }); }); describe('Edge cases', () => { From dfc6a162ca4bfbe81672bea52029240b6a757bc0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 17 Mar 2026 11:54:50 -0700 Subject: [PATCH 35/48] feat: add logo-grey image and TypeScript declaration for PNG assets --- src/core/assets/index.ts | 3 +++ src/core/assets/logo-grey.png | Bin 0 -> 3986 bytes src/types/assets.d.ts | 7 +++++++ 3 files changed, 10 insertions(+) create mode 100644 src/core/assets/index.ts create mode 100644 src/core/assets/logo-grey.png create mode 100644 src/types/assets.d.ts diff --git a/src/core/assets/index.ts b/src/core/assets/index.ts new file mode 100644 index 000000000..b2fe2a8a6 --- /dev/null +++ b/src/core/assets/index.ts @@ -0,0 +1,3 @@ +import IterableLogoGrey from './logo-grey.png'; + +export { IterableLogoGrey }; diff --git a/src/core/assets/logo-grey.png b/src/core/assets/logo-grey.png new file mode 100644 index 0000000000000000000000000000000000000000..5c0d56a926c317223c1d46fd3ed3c1e63f193fe5 GIT binary patch literal 3986 zcmV;D4{h*?P)Dg|00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91Z=eGJ1ONa40RR91ZvX%Q0PW%XN&o;3{7FPXRCodHoo$F+M;XUw=G?p4 zZ8vG6-8PVyU6UeR1MNbv)R5o?3pRo@ZBgO}X^Mg$ih>}DA0!l^w4ZD#N~ua)`^67x zT5N3##cs{Ml-8OR#1={F&2B=rHpHzPO}cx}8UN3@cP{6ioO|zinVBI zYf1ESCc5;;_U+p@!TsPhfV;hbGMl9z939)abLq_D;-3r1Ddo|=gh${!#sQ9P)sSn0 zka2i)`}PkP&YpckgFI^@`mWlXy#4mEKzOf7bVzjPk`(;l^z`(UZn>u9^%cCmd-qna z-7xq!6X2Q|yo@$*9Btr|sSMXCSLhUGQdR@p(6OZg-czDZZ&JIMJYejq1z?b@|atJPLJgmL1s^Y9cnR1sg2lHFq^*QESUAmw=g zyy!AOr>ji`4!|k^cZU^l6ba{dQk?aAeZu7`)LD2@a3M4)Zwq}J|D@c?gMfx1P6Koj zUK|{zvZ$Ri-PX1JEVvLiH7_^|&^dT&?V_A|yc7sPMln$^ocv-+vPDv?_lW)Ue zdTc&rJf#9G1zuKgl#wV2Z^E-vbxQtBN_Z5kG=SFUHuHc}83}_TnkYV?&W-q-zbplN zC8KN+1i=lq=R&QPydvNzGqG5T62Yki8gbOt!NM#{sf+4iy#!h-c}2mgT=H00ebOYg z{^>H#et=SUO2w7Kw^{TkXdZe}Zo@Lsv*;NXn)gIic`1;=j{)xGsC*5_^-85O50i>Z8e}DkFk13VfuodA z2}FbaDy*Ri-mb*d^|~n8U(t?!7qb8-dj-vL-*^c$R`N`Oi)N${Vna|54hSZXpcJO+ zi6YVO22B1MD+hnT^QKK5jFmtm@N9uo5D6eAugB4DZNr@8&44({nH&x8bACNW_!n^e z0q5cjfc?pr{s%b!hXrs-MI!*U!?O)e<&FY~_N!|rCSU>BS_P+6)DBQ z``qLIQiVkJ6Fl;2XN=lpFayz^(*+R@7}%p0$^Sbkcqv}lELOK zftlg;6F3EBy#mYxuOGp!XMm~U^)tBj3NY35SQK1ERyO&<$8wCkS!D{^l5<|*y<6vX zJ2oaN;-*iX6V=AWZY7@L&7Z-wwY3T8J(w84&c$K8%rcA>4SJ#F5>`pbT1_n5P3L(_&?7XpTLiwptU=! zEn=Q;9w4U~XD{|z59|t$z+E#i_y`1j4Y6c0I{?JvoOzF%gqRnc$}wvIB>`hc0p@C8 zN}0G%wxg2$tTIq}q|<_KM|iaBg710HV|aT@TyENP2tUUJe;ms&+HEe2f>UWQ2@pTv zt=KE(dzc}h5|`8QvvuhooSU1w%C;u4!$SL^?%BR$`)iAfi)~iiSHYta*95n@K!ou}Zv(&;04%)A zkWO$)N!rmpJ$ke_?mFRxlwpUu)+-9)0cFY_&syl;+vErS+(T*g? zYL#{byb_5z^&#fD2GIR~zz{gnW6WP$drB$Psy4%Ip}6>^x?#v4#;>y4wAM;(_+tZ{ zN^jmOl)%A!@HX@izpi8%^2c_Lj=sLIuuyB2JYRUn3`G_fdzkw$3S#MARkjIEDVnte zh9wRg5XgXiQOPjrW4su$YQaNfhfPKpkv+@=yCn_4GQcT4(*jiBu<6cq-6t@VWDPzx zg{A&U2#x>ZG%1keR=O21A3qu?kk+Pb3SITd9(U$9$4co;7fm;p!^S2Psr?C2gRZ6(kp za7yPmz&JPxhdlh43U9;5yNVOhHqS)03Wg1EYW4-H3K;~aM8hgA5;eaZIWc~SYI-?@haxy{Jho^AoWG|| zqu|sv*8nPTnEDDAZv*_u;sB9{7nS}te301z;SU(k~8> zb7^JqS;dcq@u?7Ab4T;#)(yi4lJU82;8gTw1MCD&=@$ivlJ^&N-G=XR*?>opVUYQl zjYv`I-p9+3^Qw*O6=zuhdx2AWM*x0^v|Y7es={gZYg4nc^Q{hx7^pmo2#l9L3}%0k z7Mx13ZGdUPDg6{cykh-Zb4fr*D!0LXI+bVhC2}&P*;mSaS z4UtlOH4iwYkLePa7o5_MN+7lw`615RhC=GT4xc_;rR221!z{wG@cp|{ch^(^js8QZ zE*Fc+GX=0HIF+t&w?J&hqt12ucp>}SU{q5wTh*qzP&<2e`4ii=Jr6sU!??CtT`%ZA z)Yy8epp@q0x~ouK;99AJ*`AP`He21_x>oQ2J@f}>=4Kv_in$6N-BbXj@IH;xozVxf z9HRtd>?0n)mQcUT@?4s*x&Uei*Hj*zms3dp3__s-1BBpPjjOLwQHJj|7XP!N|zSCD=962P)cz81b-Wo%d=3*AqC( zsNs55gf5d)nTE4an~i@HlBUa^VDrz^dUOV4UZmn z9U$gWH=s9n1O9x{*Fl#X9Hk{wn-@>K%WWWm3-{m*}64()3Lq!(=Y6C|}(gu%& zl?kvXI8u;Yc_mOQI7&*P-X;l1a<+S+3Y;JKTQE;JjzrxIy+&Z$c#YBz23BWqyZ|$W zchLL1fGv$L!vD9rZ`jukj`TGGj|AHSNZ??Nb`Q4Z{~XNk$bO^+jPyuy2=^{_djAb3 z`DQHuGy;y&rM23OW|WO8w92Ymf}nm3zrb%G?os?zqdIgtM)>Pde;xq19hjtj55_|u zb_!eYx-oF1moa!G+$6xpveX*ALr<&yxf~!8@cr<$7c`DI4OCT)Q*bybe zq#u4*IQ=GfrGTUKmu8Fd_Sq$TI>0}wMIn{2doq>pm6U57eO@Jk0rI+YHOl;{Oe zNMXE_LV849z*yTxKaqP*fFpgJfJcgS1?Z*QA&}%4cFusKbUFi%l<5f2#}>!sJEy== zb~pu(6iNapc#!EO5|UHYA!DLzea?ZS^f?EQl!^oN@x>Hewh9oPq$Sb?&Pi~jn3M2G zu{c0X7~}OZI}WSyOU)GqXTecUI17)Iivq+uYuZ1oL2)EG3`=NJ1*gH0-cG|K1r@-m zz;cbPs#EZv_xg?x=~lKYWo|QTqG>Ml<0B&%@rpH>IZS4KK3jPHf>-VQ{hn;H%!V#y zRUvPZXf@*6!J$8*GjU5^8Qq`j{N0}J&y{o6`D!zoEmVaHzkV04El00wJNEcDlRmhl zO}=J9eFcxwKJ~^MHJ{0YIMfP|_BlCm;>2^B^Vg`jzN$8(IYD0UmOPaJ z@(=9WcNE)g-3R-Y$>{Y?#~6g;V^6_j*gfy*PGS1t`nrIpGCKU)rVx`DzTSn>xEbGX s7*qGqrp*KH>k?ZsH+S|`xBAxqFNL) Date: Tue, 17 Mar 2026 11:56:23 -0700 Subject: [PATCH 36/48] feat: add IterableEmbeddedCard component with default export --- .../{ => IterableEmbeddedCard}/IterableEmbeddedCard.tsx | 2 +- src/embedded/components/IterableEmbeddedCard/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) rename src/embedded/components/{ => IterableEmbeddedCard}/IterableEmbeddedCard.tsx (82%) create mode 100644 src/embedded/components/IterableEmbeddedCard/index.ts diff --git a/src/embedded/components/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx similarity index 82% rename from src/embedded/components/IterableEmbeddedCard.tsx rename to src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 87b2d1940..d60d55a63 100644 --- a/src/embedded/components/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,5 +1,5 @@ import { View, Text } from 'react-native'; -import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps'; +import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; export const IterableEmbeddedCard = ({ config, diff --git a/src/embedded/components/IterableEmbeddedCard/index.ts b/src/embedded/components/IterableEmbeddedCard/index.ts new file mode 100644 index 000000000..97ca487ef --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/index.ts @@ -0,0 +1,2 @@ +export * from './IterableEmbeddedCard'; +export { IterableEmbeddedCard as default } from './IterableEmbeddedCard'; From 2e983a8b0c6291b84d676d4faf30da3f42c798dd Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 17 Mar 2026 12:08:55 -0700 Subject: [PATCH 37/48] feat: implement IterableEmbeddedCard component --- .../IterableEmbeddedCard.styles.ts | 95 +++++++++++++ .../IterableEmbeddedCard.tsx | 127 +++++++++++++++++- 2 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts new file mode 100644 index 000000000..8f51e2ff7 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.styles.ts @@ -0,0 +1,95 @@ +import { StyleSheet } from 'react-native'; + +export const IMAGE_HEIGHT = 230; +export const PLACEHOLDER_IMAGE_HEIGHT = 56; +export const PLACEHOLDER_IMAGE_WIDTH = 56; +const SHADOW_COLOR_LIGHT = 'rgba(0, 0, 0, 0.06)'; +const SHADOW_COLOR_DARK = 'rgba(0, 0, 0, 0.08)'; +const IMAGE_BACKGROUND_COLOR = '#F5F4F4'; + +export const styles = StyleSheet.create({ + body: { + alignSelf: 'stretch', + fontSize: 14, + fontWeight: '400', + lineHeight: 20, + }, + bodyContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'column', + gap: 24, + paddingBottom: 16, + paddingHorizontal: 16, + paddingTop: 12, + }, + button: { + borderRadius: 32, + gap: 8, + }, + buttonContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + gap: 12, + width: '100%', + }, + buttonText: { + fontSize: 14, + fontWeight: '700', + lineHeight: 20, + }, + container: { + alignItems: 'center', + borderStyle: 'solid', + boxShadow: + `0 1px 1px 0 ${SHADOW_COLOR_LIGHT}, 0 0 2px 0 ${SHADOW_COLOR_LIGHT}, 0 0 1px 0 ${SHADOW_COLOR_DARK}`, + display: 'flex', + elevation: 1, + flexDirection: 'column', + gap: 16, + justifyContent: 'center', + overflow: 'hidden', + shadowColor: SHADOW_COLOR_LIGHT, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.9, + shadowRadius: 2, + width: '100%', + }, + mediaContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + backgroundColor: IMAGE_BACKGROUND_COLOR, + display: 'flex', + flexDirection: 'row', + height: IMAGE_HEIGHT, + }, + mediaContainerNoImage: { + alignItems: 'center', + justifyContent: 'center', + }, + mediaImage: { + height: IMAGE_HEIGHT, + paddingHorizontal: 0, + paddingVertical: 0, + width: '100%', + }, + mediaImagePlaceholder: { + height: PLACEHOLDER_IMAGE_HEIGHT, + opacity: 0.25, + width: PLACEHOLDER_IMAGE_WIDTH, + }, + textContainer: { + alignItems: 'flex-start', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'column', + gap: 8, + }, + title: { + fontSize: 18, + fontWeight: '700', + }, +}); diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index d60d55a63..4c9148c2f 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -1,18 +1,131 @@ -import { View, Text } from 'react-native'; +import { + Image, + PixelRatio, + Pressable, + Text, + TouchableOpacity, + View, + type TextStyle, + type ViewStyle, +} from 'react-native'; + +import { IterableLogoGrey } from '../../../core/assets'; +import { IterableEmbeddedViewType } from '../../enums'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; +import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; + +/** + * TODO: Add default action click handler. See IterableEmbeddedView for functionality. + */ export const IterableEmbeddedCard = ({ config, message, onButtonClick = () => {}, + onMessageClick = () => {}, }: IterableEmbeddedComponentProps) => { - console.log(`🚀 > IterableEmbeddedCard > config:`, config); - console.log(`🚀 > IterableEmbeddedCard > message:`, message); - console.log(`🚀 > IterableEmbeddedCard > onButtonClick:`, onButtonClick); + const { + handleButtonClick, + handleMessageClick, + media, + parsedStyles, + } = useEmbeddedView(IterableEmbeddedViewType.Card, { + message, + config, + onButtonClick, + onMessageClick, + }); + const buttons = message?.elements?.buttons ?? []; return ( - - IterableEmbeddedCard - + handleMessageClick()}> + + + {media.caption + + + + + {message.elements?.title} + + + {message.elements?.body} + + + {buttons.length > 0 && ( + + {buttons.map((button, index) => { + const backgroundColor = + index === 0 + ? parsedStyles.primaryBtnBackgroundColor + : parsedStyles.secondaryBtnBackgroundColor; + const textColor = + index === 0 + ? parsedStyles.primaryBtnTextColor + : parsedStyles.secondaryBtnTextColor; + return ( + handleButtonClick(button)} + key={button.id} + > + + {button.title} + + + ); + })} + + )} + + + ); }; From 96ad2c66d2f293163ad08c70069c4fa58559ee08 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 17 Mar 2026 12:25:12 -0700 Subject: [PATCH 38/48] test: add unit tests for IterableEmbeddedCard component --- .../IterableEmbeddedCard.test.tsx | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx new file mode 100644 index 000000000..c423595e9 --- /dev/null +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.test.tsx @@ -0,0 +1,371 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { fireEvent, render } from '@testing-library/react-native'; + +import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType'; +import { useEmbeddedView } from '../../hooks/useEmbeddedView'; +import type { IterableEmbeddedMessage } from '../../types/IterableEmbeddedMessage'; +import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton'; +import { IterableLogoGrey } from '../../../core/assets'; +import { + IMAGE_HEIGHT, + PLACEHOLDER_IMAGE_HEIGHT, + PLACEHOLDER_IMAGE_WIDTH, +} from './IterableEmbeddedCard.styles'; +import { IterableEmbeddedCard } from './IterableEmbeddedCard'; + +const mockHandleButtonClick = jest.fn(); +const mockHandleMessageClick = jest.fn(); + +jest.mock('../../hooks/useEmbeddedView', () => ({ + useEmbeddedView: jest.fn(), +})); + +const mockUseEmbeddedView = useEmbeddedView as jest.MockedFunction< + typeof useEmbeddedView +>; + +const defaultParsedStyles = { + backgroundColor: '#ffffff', + borderColor: '#E0DEDF', + borderCornerRadius: 8, + borderWidth: 1, + primaryBtnBackgroundColor: '#6A266D', + primaryBtnTextColor: '#ffffff', + secondaryBtnBackgroundColor: 'transparent', + secondaryBtnTextColor: '#79347F', + titleTextColor: '#3D3A3B', + bodyTextColor: '#787174', +}; + +function mockUseEmbeddedViewReturn( + overrides: Partial> = {} +) { + mockUseEmbeddedView.mockReturnValue({ + parsedStyles: defaultParsedStyles, + handleButtonClick: mockHandleButtonClick, + handleMessageClick: mockHandleMessageClick, + media: { url: null, caption: null, shouldShow: false }, + ...overrides, + }); +} + +describe('IterableEmbeddedCard', () => { + const baseMessage: IterableEmbeddedMessage = { + metadata: { + messageId: 'msg-1', + campaignId: 1, + placementId: 1, + }, + elements: { + title: 'Card Title', + body: 'Card body text.', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseEmbeddedViewReturn(); + }); + + describe('Rendering', () => { + it('should render without crashing', () => { + const { getByText } = render(); + expect(getByText('Card Title')).toBeTruthy(); + expect(getByText('Card body text.')).toBeTruthy(); + }); + + it('should render title and body from message.elements', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + title: 'Custom Card Title', + body: 'Custom card body.', + }, + }; + const { getByText } = render(); + expect(getByText('Custom Card Title')).toBeTruthy(); + expect(getByText('Custom card body.')).toBeTruthy(); + }); + + it('should apply parsedStyles to container and text', () => { + const customStyles = { + ...defaultParsedStyles, + backgroundColor: '#000000', + titleTextColor: '#ff0000', + bodyTextColor: '#00ff00', + }; + mockUseEmbeddedViewReturn({ parsedStyles: customStyles }); + + const { getByText, UNSAFE_getAllByType } = render( + + ); + + const views = UNSAFE_getAllByType('View' as any); + const styleArray = (s: any) => (Array.isArray(s) ? s : [s]); + const container = views.find( + (v: any) => + v.props.style && + styleArray(v.props.style).some( + (sty: any) => sty && sty.backgroundColor === '#000000' + ) + ); + expect(container).toBeTruthy(); + expect(styleArray(container!.props.style)).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + backgroundColor: '#000000', + borderColor: customStyles.borderColor, + borderRadius: customStyles.borderCornerRadius, + borderWidth: customStyles.borderWidth, + }), + ]) + ); + + const title = getByText('Card Title'); + const body = getByText('Card body text.'); + expect(title.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#ff0000' }), + ]) + ); + expect(body.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ color: '#00ff00' }), + ]) + ); + }); + + it('should not render button container when message has no buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: undefined }, + }; + const { queryByText } = render(); + expect(queryByText('Primary')).toBeNull(); + }); + + it('should not render button container when buttons array is empty', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { ...baseMessage.elements, buttons: [] }, + }; + const { queryByText } = render(); + expect(queryByText('Primary')).toBeNull(); + }); + }); + + describe('Buttons', () => { + const primaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-primary', + title: 'Primary', + action: { type: 'openUrl', data: 'https://example.com' }, + }; + const secondaryButton: IterableEmbeddedMessageElementsButton = { + id: 'btn-secondary', + title: 'Secondary', + }; + + it('should render buttons when message has buttons', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render(); + expect(getByText('Primary')).toBeTruthy(); + expect(getByText('Secondary')).toBeTruthy(); + }); + + it('should apply primary and secondary button text colors from parsedStyles', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render(); + + const primaryText = getByText('Primary'); + const secondaryText = getByText('Secondary'); + expect(primaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.primaryBtnTextColor, + }), + ]) + ); + expect(secondaryText.props.style).toEqual( + expect.arrayContaining([ + expect.any(Object), + expect.objectContaining({ + color: defaultParsedStyles.secondaryBtnTextColor, + }), + ]) + ); + }); + + it('should call handleButtonClick with correct button when button is pressed', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [primaryButton, secondaryButton], + }, + }; + const { getByText } = render(); + + fireEvent.press(getByText('Primary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(1); + expect(mockHandleButtonClick).toHaveBeenCalledWith(primaryButton); + + fireEvent.press(getByText('Secondary')); + expect(mockHandleButtonClick).toHaveBeenCalledTimes(2); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith(secondaryButton); + }); + }); + + describe('Media', () => { + it('should render placeholder image when media.shouldShow is false', () => { + const { UNSAFE_getAllByType } = render( + + ); + + const images = UNSAFE_getAllByType('Image' as any); + expect(images.length).toBeGreaterThan(0); + + const image = images[0] as any; + const styleArray = (s: any) => (Array.isArray(s) ? s : [s]); + + expect(styleArray(image.props.style)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + height: PLACEHOLDER_IMAGE_HEIGHT, + width: PLACEHOLDER_IMAGE_WIDTH, + }), + ]) + ); + expect(image.props.source).toBe(IterableLogoGrey); + }); + + it('should render media image when media.shouldShow is true', () => { + const media = { + url: 'https://example.com/image.png', + caption: 'Card image', + shouldShow: true, + }; + mockUseEmbeddedViewReturn({ media }); + + const { UNSAFE_getAllByType } = render( + + ); + + const images = UNSAFE_getAllByType('Image' as any); + expect(images.length).toBeGreaterThan(0); + + const image = images[0] as any; + expect(image.props.source.uri).toBe(media.url); + + const styleArray = (s: any) => (Array.isArray(s) ? s : [s]); + expect(styleArray(image.props.style)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + height: IMAGE_HEIGHT, + }), + ]) + ); + }); + }); + + describe('Message click', () => { + it('should call handleMessageClick when card is pressed', () => { + const { getByText } = render(); + + fireEvent.press(getByText('Card Title')); + expect(mockHandleMessageClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('useEmbeddedView integration', () => { + it('should call useEmbeddedView with Card viewType and props', () => { + const config = { backgroundColor: '#abc' } as any; + const onButtonClick = jest.fn(); + const onMessageClick = jest.fn(); + + render( + + ); + + expect(mockUseEmbeddedView).toHaveBeenCalledTimes(1); + expect(mockUseEmbeddedView).toHaveBeenCalledWith( + IterableEmbeddedViewType.Card, + { + message: baseMessage, + config, + onButtonClick, + onMessageClick, + } + ); + }); + }); + + describe('Edge cases', () => { + it('should handle message with missing elements', () => { + const message: IterableEmbeddedMessage = { + metadata: baseMessage.metadata, + elements: undefined, + }; + const { queryByText } = render(); + expect(queryByText('Card Title')).toBeNull(); + expect(queryByText('Card body text.')).toBeNull(); + }); + + it('should handle message with empty title and body without throwing', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { title: '', body: '' }, + }; + const { getAllByText } = render( + + ); + const emptyTextNodes = getAllByText(''); + expect(emptyTextNodes.length).toBeGreaterThanOrEqual(1); + }); + + it('should render multiple buttons and call handleButtonClick with correct button for each', () => { + const message: IterableEmbeddedMessage = { + ...baseMessage, + elements: { + ...baseMessage.elements, + buttons: [ + { id: 'unique-id-1', title: 'First' }, + { id: 'unique-id-2', title: 'Second' }, + ], + }, + }; + const { getByText } = render(); + expect(getByText('First')).toBeTruthy(); + expect(getByText('Second')).toBeTruthy(); + fireEvent.press(getByText('First')); + expect(mockHandleButtonClick).toHaveBeenCalledWith( + expect.objectContaining({ id: 'unique-id-1', title: 'First' }) + ); + fireEvent.press(getByText('Second')); + expect(mockHandleButtonClick).toHaveBeenLastCalledWith( + expect.objectContaining({ id: 'unique-id-2', title: 'Second' }) + ); + }); + }); +}); + From c72f9d12cfc694d2ec01ab8e3db7ba7297815507 Mon Sep 17 00:00:00 2001 From: Loren <3190869+lposen@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:26:45 -0700 Subject: [PATCH 39/48] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../IterableEmbeddedBanner/IterableEmbeddedBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 5deaa17d0..92d568e30 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -39,7 +39,7 @@ export const IterableEmbeddedBanner = ({ const buttons = message.elements?.buttons ?? []; return ( - handleMessageClick()}> + Date: Mon, 6 Apr 2026 14:42:49 -0700 Subject: [PATCH 40/48] refactor: remove TODO comments --- .../IterableEmbeddedBanner.tsx | 4 --- .../IterableEmbeddedCard.tsx | 25 ++++++------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 92d568e30..e02b01f55 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -18,10 +18,6 @@ import { IMAGE_WIDTH, } from './IterableEmbeddedBanner.styles'; -/** - * TODO: figure out how default action works. - */ - export const IterableEmbeddedBanner = ({ config, message, diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 4c9148c2f..236d90f7d 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -15,27 +15,19 @@ import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; -/** - * TODO: Add default action click handler. See IterableEmbeddedView for functionality. - */ - export const IterableEmbeddedCard = ({ config, message, 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 ( @@ -64,8 +56,7 @@ export const IterableEmbeddedCard = ({ uri: media.url as string, height: PixelRatio.getPixelSizeForLayoutSize(IMAGE_HEIGHT), } - : - IterableLogoGrey + : IterableLogoGrey } style={ media.shouldShow From 83f65b4d18ea06fed5145e4847a4147dcd19c15c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 6 Apr 2026 15:04:49 -0700 Subject: [PATCH 41/48] docs: enhance IterableEmbedded components with documentation --- .../IterableEmbeddedBanner.tsx | 20 +++++++ .../IterableEmbeddedCard.tsx | 20 +++++++ .../IterableEmbeddedNotification.tsx | 20 +++++++ .../components/IterableEmbeddedView.tsx | 54 ++++++++++++++++++- .../hooks/useEmbeddedView/getMedia.ts | 2 + .../hooks/useEmbeddedView/getStyles.ts | 2 + .../hooks/useEmbeddedView/useEmbeddedView.ts | 2 + src/index.tsx | 1 + 8 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index e02b01f55..1fbb8dfee 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -18,6 +18,26 @@ import { IMAGE_WIDTH, } from './IterableEmbeddedBanner.styles'; +/** + * + * @param config - The config for the IterableEmbeddedBanner component. + * @param message - The message to render. + * @param onButtonClick - The function to call when a button is clicked. + * @param onMessageClick - The function to call when the message is clicked. + * @returns The IterableEmbeddedBanner component. + * + * @example + * ```tsx + * return ( + * + * ); + * ``` + */ export const IterableEmbeddedBanner = ({ config, message, diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index 236d90f7d..da901d642 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -15,6 +15,26 @@ import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; +/** + * + * @param config - The config for the IterableEmbeddedCard component. + * @param message - The message to render. + * @param onButtonClick - The function to call when a button is clicked. + * @param onMessageClick - The function to call when the message is clicked. + * @returns The IterableEmbeddedCard component. + * + * @example + * ```tsx + * return ( + * + * ); + * ``` + */ export const IterableEmbeddedCard = ({ config, message, diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index 856ef4da9..1e75c9a75 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -12,6 +12,26 @@ import { useEmbeddedView } from '../../hooks/useEmbeddedView'; import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps'; import { styles } from './IterableEmbeddedNotification.styles'; +/** + * + * @param config - The config for the IterableEmbeddedNotification component. + * @param message - The message to render. + * @param onButtonClick - The function to call when a button is clicked. + * @param onMessageClick - The function to call when the message is clicked. + * @returns The IterableEmbeddedNotification component. + * + * @example + * ```tsx + * return ( + * + * ); + * ``` + */ export const IterableEmbeddedNotification = ({ config, message, diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index 86844f15f..de380516b 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -9,7 +9,7 @@ import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; /** * The props for the IterableEmbeddedView component. */ -interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { +export interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { /** The type of view to render. */ viewType: IterableEmbeddedViewType; } @@ -20,10 +20,62 @@ interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { * @param message - The message to render. * @param config - The config for the IterableEmbeddedView component, most likely used to style the view. * @param onButtonClick - The function to call when a button is clicked. + * @param onMessageClick - The function to call when the message is clicked. * @returns The IterableEmbeddedView component. * * This component is used to render pre-created, customizable message displays * included with Iterables RN SDK: cards, banners, and notifications. + * + * @example + * ```tsx + * // See `IterableEmbeddedViewType`` for available view types. + * const viewType = IterableEmbeddedViewType.Card; + * + * // The message object that will be rendered. + * // You can retrieve messages by calling `Iterable.embeddedManager.getMessages(IDS)` + * const message = { + * metadata: { + * messageId: 'test-message-123', + * campaignId: 123456, + * placementId: 'test-placement', + * }, + * elements: { + * title: 'Test Title', + * body: 'Test Body', + * buttons: [ + * { id: 'button-1', label: 'Button 1', action: 'button-1-action' }, + * { id: 'button-2', label: 'Button 2', action: 'button-2-action' }, + * ], + * }, + * }; + * + * // The config for the IterableEmbeddedView component, most likely used to style the view. + * // See `IterableEmbeddedViewConfig` for available config options. + * const config = { backgroundColor: '#FFFFFF', borderRadius: 8 }; + * + * // A callback that will be called when a button is clicked. + * // General click handling is handled by the SDK; This is only for custom logic. + * const onButtonClick = () => { + * console.log('Button clicked'); + * }; + * + * // A callback that will be called when the message is clicked. + * // This is not called when a button is clicked. + * // If a default action is set, this is what will be called. + * const onMessageClick = () => { + * console.log('Message clicked'); + * }; + * + * return ( + * + * ); + * ``` */ export const IterableEmbeddedView = ({ viewType, diff --git a/src/embedded/hooks/useEmbeddedView/getMedia.ts b/src/embedded/hooks/useEmbeddedView/getMedia.ts index ecc63ea3b..1e138d52d 100644 --- a/src/embedded/hooks/useEmbeddedView/getMedia.ts +++ b/src/embedded/hooks/useEmbeddedView/getMedia.ts @@ -10,10 +10,12 @@ import { IterableEmbeddedViewType } from '../../enums'; * @returns The media to render. * * @example + * ```ts * const media = getMedia(IterableEmbeddedViewType.Notification, message); * console.log(media.url); * console.log(media.caption); * console.log(media.shouldShow ? 'true' : 'false'); + * ``` */ export const getMedia = ( /** The type of view to render. */ diff --git a/src/embedded/hooks/useEmbeddedView/getStyles.ts b/src/embedded/hooks/useEmbeddedView/getStyles.ts index de2c69206..5781c1a84 100644 --- a/src/embedded/hooks/useEmbeddedView/getStyles.ts +++ b/src/embedded/hooks/useEmbeddedView/getStyles.ts @@ -37,6 +37,7 @@ const getDefaultStyle = ( * @returns The styles. * * @example + * ```ts * const styles = getStyles(IterableEmbeddedViewType.Notification, { * backgroundColor: '#000000', * borderColor: '#000000', @@ -45,6 +46,7 @@ const getDefaultStyle = ( * primaryBtnBackgroundColor: '#000000', * primaryBtnTextColor: '#000000', * }); + * ``` */ export const getStyles = ( viewType: IterableEmbeddedViewType, diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index 7dcd663da..22b9be9f5 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -16,6 +16,7 @@ const noop = () => {}; * @returns The embedded view. * * @example + * ```tsx * const { handleButtonClick, handleMessageClick, media, parsedStyles } = useEmbeddedView(IterableEmbeddedViewType.Notification, { * message, * config, @@ -30,6 +31,7 @@ const noop = () => {}; * {parsedStyles.backgroundColor} * * ); + * ``` */ export const useEmbeddedView = ( /** The type of view to render. */ diff --git a/src/index.tsx b/src/index.tsx index b4ba8f5cf..13cb6915d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -42,6 +42,7 @@ export { type IterableEmbeddedMessageElementsButton, type IterableEmbeddedMessageElementsText, type IterableEmbeddedViewConfig, + type IterableEmbeddedViewProps, } from './embedded'; export { IterableHtmlInAppContent, From 186e9d94940bc7ddaadeb8ea4023e1b59c99a0e2 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 6 Apr 2026 15:20:05 -0700 Subject: [PATCH 42/48] docs: update IterableEmbeddedViewType enum with detailed descriptions and links to OOTB views --- src/embedded/enums/IterableEmbeddedViewType.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/embedded/enums/IterableEmbeddedViewType.ts b/src/embedded/enums/IterableEmbeddedViewType.ts index 90a0b5d7e..89873120c 100644 --- a/src/embedded/enums/IterableEmbeddedViewType.ts +++ b/src/embedded/enums/IterableEmbeddedViewType.ts @@ -2,10 +2,16 @@ * The view type for an embedded message. */ export enum IterableEmbeddedViewType { - /** The embedded view is a banner */ + /** + * [Banner](https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages#banners) Out of the Box (OOTB) view. + */ Banner = 0, - /** The embedded view is a card */ + /** + * [Card](https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages#cards) Out of the Box (OOTB) view. + */ Card = 1, - /** The embedded view is a notification */ + /** + * [Notification](https://support.iterable.com/hc/en-us/articles/23230946708244-Out-of-the-Box-Views-for-Embedded-Messages#notifications) Out of the Box (OOTB) view. + */ Notification = 2, } From c758bcbf7ebd763d63cd886ef91ac69dd900cceb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 6 Apr 2026 15:31:47 -0700 Subject: [PATCH 43/48] docs: improve IterableEmbeddedView documentation and update prop interface for clarity --- .../components/IterableEmbeddedView.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index de380516b..036f767a2 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -9,7 +9,8 @@ import { IterableEmbeddedNotification } from './IterableEmbeddedNotification'; /** * The props for the IterableEmbeddedView component. */ -export interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProps { +export interface IterableEmbeddedViewProps + extends IterableEmbeddedComponentProps { /** The type of view to render. */ viewType: IterableEmbeddedViewType; } @@ -23,45 +24,62 @@ export interface IterableEmbeddedViewProps extends IterableEmbeddedComponentProp * @param onMessageClick - The function to call when the message is clicked. * @returns The IterableEmbeddedView component. * - * This component is used to render pre-created, customizable message displays - * included with Iterables RN SDK: cards, banners, and notifications. + * This component is used to render the following pre-created, customizable + * message displays included with Iterables RN SDK: cards, banners, and + * notifications. * * @example * ```tsx - * // See `IterableEmbeddedViewType`` for available view types. + * import { + * IterableAction, + * IterableEmbeddedView, + * IterableEmbeddedViewType, + * type IterableEmbeddedMessage, + * type IterableEmbeddedMessageElementsButton, + * } from '@iterable/react-native-sdk'; + * + * // See `IterableEmbeddedViewType` for available view types. * const viewType = IterableEmbeddedViewType.Card; * - * // The message object that will be rendered. - * // You can retrieve messages by calling `Iterable.embeddedManager.getMessages(IDS)` - * const message = { + * // Messages usually come from the embedded manager. `placementIds` is `number[] | null` + * // (use `null` to load messages for all placements), for example: + * // Iterable.embeddedManager.getMessages([101, 102]).then((messages) => { ... }); + * const message: IterableEmbeddedMessage = { * metadata: { * messageId: 'test-message-123', + * placementId: 101, * campaignId: 123456, - * placementId: 'test-placement', * }, * elements: { * title: 'Test Title', * body: 'Test Body', * buttons: [ - * { id: 'button-1', label: 'Button 1', action: 'button-1-action' }, - * { id: 'button-2', label: 'Button 2', action: 'button-2-action' }, + * { + * id: 'button-1', + * title: 'Button 1', + * action: new IterableAction('openUrl', 'https://example.com/one'), + * }, + * { + * id: 'button-2', + * title: 'Button 2', + * action: new IterableAction('openUrl', 'https://example.com/two'), + * }, * ], * }, * }; * - * // The config for the IterableEmbeddedView component, most likely used to style the view. + * // The config is used to style the component. * // See `IterableEmbeddedViewConfig` for available config options. * const config = { backgroundColor: '#FFFFFF', borderRadius: 8 }; * - * // A callback that will be called when a button is clicked. - * // General click handling is handled by the SDK; This is only for custom logic. - * const onButtonClick = () => { - * console.log('Button clicked'); + * // `onButtonClick` will be called when a button is clicked. + * // This callback allows you to add custom logic in addition to the SDK's default handling. + * const onButtonClick = (button: IterableEmbeddedMessageElementsButton) => { + * console.log('Button clicked', button.id, button.title, button.action); * }; * - * // A callback that will be called when the message is clicked. - * // This is not called when a button is clicked. - * // If a default action is set, this is what will be called. + * // `onMessageClick` will be called when the message is clicked anywhere outside of a button. + * // If a default action is set, it will be handled prior to this callback. * const onMessageClick = () => { * console.log('Message clicked'); * }; From d94b4d3831b759a98de826a5f66a997a914c4494 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Apr 2026 13:23:35 -0700 Subject: [PATCH 44/48] docs: remove campaignId from IterableEmbeddedViewProps documentation for accuracy --- src/embedded/components/IterableEmbeddedView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/embedded/components/IterableEmbeddedView.tsx b/src/embedded/components/IterableEmbeddedView.tsx index 036f767a2..f334bc647 100644 --- a/src/embedded/components/IterableEmbeddedView.tsx +++ b/src/embedded/components/IterableEmbeddedView.tsx @@ -48,7 +48,6 @@ export interface IterableEmbeddedViewProps * metadata: { * messageId: 'test-message-123', * placementId: 101, - * campaignId: 123456, * }, * elements: { * title: 'Test Title', From edcd445274519f2bf87d75fd2bba00586e5c2168 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Apr 2026 13:31:41 -0700 Subject: [PATCH 45/48] refactor: update onButtonClick prop to include message parameter --- src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts | 2 +- src/embedded/types/IterableEmbeddedComponentProps.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index 22b9be9f5..1e7785335 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -53,7 +53,7 @@ export const useEmbeddedView = ( const handleButtonClick = useCallback( (button: IterableEmbeddedMessageElementsButton) => { - onButtonClick(button); + onButtonClick(button, message); Iterable.embeddedManager.handleClick(message, button.id, button.action); }, [onButtonClick, message] diff --git a/src/embedded/types/IterableEmbeddedComponentProps.ts b/src/embedded/types/IterableEmbeddedComponentProps.ts index f59e2772e..400dc1ccd 100644 --- a/src/embedded/types/IterableEmbeddedComponentProps.ts +++ b/src/embedded/types/IterableEmbeddedComponentProps.ts @@ -8,7 +8,10 @@ export interface IterableEmbeddedComponentProps { /** The config for the embedded view. */ config?: IterableEmbeddedViewConfig | null; /** The function to call when a button is clicked. */ - onButtonClick?: (button: IterableEmbeddedMessageElementsButton) => void; + onButtonClick?: ( + button: IterableEmbeddedMessageElementsButton, + message: IterableEmbeddedMessage + ) => void; /** The function to call when the message is clicked. */ onMessageClick?: () => void; } From e0c9d7ff1fdd613537d1cb85287eb8a6154c6bf3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 8 Apr 2026 13:34:16 -0700 Subject: [PATCH 46/48] docs: add component descriptions for IterableEmbeddedBanner, Card, and Notification --- .../components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx | 1 + .../components/IterableEmbeddedCard/IterableEmbeddedCard.tsx | 1 + .../IterableEmbeddedNotification.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx index 1fbb8dfee..8035b4b82 100644 --- a/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx +++ b/src/embedded/components/IterableEmbeddedBanner/IterableEmbeddedBanner.tsx @@ -19,6 +19,7 @@ import { } from './IterableEmbeddedBanner.styles'; /** + * The IterableEmbeddedBanner component is used to render a banner message. * * @param config - The config for the IterableEmbeddedBanner component. * @param message - The message to render. diff --git a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx index da901d642..1f450be7d 100644 --- a/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx +++ b/src/embedded/components/IterableEmbeddedCard/IterableEmbeddedCard.tsx @@ -16,6 +16,7 @@ import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbedde import { IMAGE_HEIGHT, styles } from './IterableEmbeddedCard.styles'; /** + * The IterableEmbeddedCard component is used to render a card message. * * @param config - The config for the IterableEmbeddedCard component. * @param message - The message to render. diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx index 1e75c9a75..6d86aecbc 100644 --- a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx +++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx @@ -13,6 +13,7 @@ import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbedde import { styles } from './IterableEmbeddedNotification.styles'; /** + * The IterableEmbeddedNotification component is used to render a notification message. * * @param config - The config for the IterableEmbeddedNotification component. * @param message - The message to render. From 78927bc2e8b36c0a8a6a2e8bcdef3744afe2383d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 16 Apr 2026 14:34:13 -0700 Subject: [PATCH 47/48] refactor: update onMessageClick to accept message parameter and adjust related tests --- src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts | 4 ++-- src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts | 4 ++-- src/embedded/types/IterableEmbeddedComponentProps.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts index 4fd06e6c2..2e065b951 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.test.ts @@ -97,7 +97,7 @@ describe('useEmbeddedView', () => { }); describe('handleButtonClick', () => { - it('calls onButtonClick with the button', () => { + it('calls onButtonClick with the button and message', () => { const onButtonClick = jest.fn(); const button: IterableEmbeddedMessageElementsButton = { id: 'btn-1', @@ -117,7 +117,7 @@ describe('useEmbeddedView', () => { }); expect(onButtonClick).toHaveBeenCalledTimes(1); - expect(onButtonClick).toHaveBeenCalledWith(button); + expect(onButtonClick).toHaveBeenCalledWith(button, minimalMessage); }); it('calls Iterable.embeddedManager.handleClick with message, button.id, and button.action', () => { diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts index 1e7785335..3ebaca600 100644 --- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts +++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts @@ -6,7 +6,7 @@ import type { IterableEmbeddedMessageElementsButton } from '../../types/Iterable import { getMedia } from './getMedia'; import { getStyles } from './getStyles'; -const noop = () => {}; +const noop = (..._args: unknown[]) => {}; /** * This hook is used to manage the lifecycle of an embedded view. @@ -60,7 +60,7 @@ export const useEmbeddedView = ( ); const handleMessageClick = useCallback(() => { - onMessageClick(); + onMessageClick(message); Iterable.embeddedManager.handleClick( message, null, diff --git a/src/embedded/types/IterableEmbeddedComponentProps.ts b/src/embedded/types/IterableEmbeddedComponentProps.ts index 400dc1ccd..f80bdd91b 100644 --- a/src/embedded/types/IterableEmbeddedComponentProps.ts +++ b/src/embedded/types/IterableEmbeddedComponentProps.ts @@ -13,5 +13,5 @@ export interface IterableEmbeddedComponentProps { message: IterableEmbeddedMessage ) => void; /** The function to call when the message is clicked. */ - onMessageClick?: () => void; + onMessageClick?: (message: IterableEmbeddedMessage) => void; } From 3b8a98d821eb14537424f05827072dec44d9845e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 22 Apr 2026 15:55:24 -0700 Subject: [PATCH 48/48] fix: apply temporary workaround for Xcode 26.4 by setting C++ language standard for fmt pod --- example/ios/Podfile | 15 +++ .../project.pbxproj | 92 ++++++++++--------- .../xcschemes/ReactNativeSdkExample.xcscheme | 2 +- 3 files changed, 64 insertions(+), 45 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e06f7e4e..5427b9f9f 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -33,5 +33,20 @@ target 'ReactNativeSdkExample' do :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + + ## Temporary workaround for Xcode 26.4 + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + # fmt/base.h unconditionally redefines FMT_USE_CONSTEVAL based on __cplusplus, + # so preprocessor defines are overwritten. The reliable fix is to compile fmt + # in C++17 mode: FMT_CPLUSPLUS (201703L) < 201709L → FMT_USE_CONSTEVAL = 0. + # All other pods stay in C++20 (React-perflogger needs std::unordered_map::contains). + if target.name == 'fmt' + config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17' + end + + end + end end end diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index fb2f1926d..ac33f65bf 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,11 +9,11 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 340B7344440A7080C4C41481 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F33C80ED83377BE42557E7A1 /* libPods-ReactNativeSdkExample.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */; }; 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */; }; 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */; }; + 7C8CB9778D44155D232C3690 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FC8A71BC6B8F9B2B3CF98A77 /* libPods-ReactNativeSdkExample.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -32,13 +32,13 @@ 00E356EE1AD99517003FC87E /* ReactNativeSdkExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactNativeSdkExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeSdkExampleTests.m; sourceTree = ""; }; - 0E36B716BFEBCD6726A94808 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeSdkExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* ReactNativeSdkExample.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = ReactNativeSdkExample.entitlements; path = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 2417D1EB7566273B17DC1E08 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; @@ -46,9 +46,9 @@ 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JwtTokenModule.mm; sourceTree = ""; }; 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtTokenModule.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; + C54F5D14A47F992CD62ED1F6 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - F33C80ED83377BE42557E7A1 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - F4BEA47FF573D84ED4C085C9 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + FC8A71BC6B8F9B2B3CF98A77 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,7 +63,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 340B7344440A7080C4C41481 /* libPods-ReactNativeSdkExample.a in Frameworks */, + 7C8CB9778D44155D232C3690 /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -107,7 +107,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - F33C80ED83377BE42557E7A1 /* libPods-ReactNativeSdkExample.a */, + FC8A71BC6B8F9B2B3CF98A77 /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -149,8 +149,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 0E36B716BFEBCD6726A94808 /* Pods-ReactNativeSdkExample.debug.xcconfig */, - F4BEA47FF573D84ED4C085C9 /* Pods-ReactNativeSdkExample.release.xcconfig */, + C54F5D14A47F992CD62ED1F6 /* Pods-ReactNativeSdkExample.debug.xcconfig */, + 2417D1EB7566273B17DC1E08 /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -180,13 +180,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - 438E21A5836E9B59D38C5C82 /* [CP] Check Pods Manifest.lock */, + 5C8B0D811BD007F9E22570D2 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 000604D101B3615CA902775E /* [CP] Embed Pods Frameworks */, - F8022DDAF9F0BC2A3E9694F4 /* [CP] Copy Pods Resources */, + 11622BAFEF48FA6292AA3DB6 /* [CP] Embed Pods Frameworks */, + 2FB5F3B3B7AA8800DE988A29 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -203,7 +203,8 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1210; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 2640; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; @@ -255,7 +256,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 000604D101B3615CA902775E /* [CP] Embed Pods Frameworks */ = { + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 11622BAFEF48FA6292AA3DB6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -272,23 +289,24 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + 2FB5F3B3B7AA8800DE988A29 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/.xcode.env", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Bundle React Native code and images"; - outputPaths = ( + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; + showEnvVarsInLog = 0; }; - 438E21A5836E9B59D38C5C82 /* [CP] Check Pods Manifest.lock */ = { + 5C8B0D811BD007F9E22570D2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -310,23 +328,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F8022DDAF9F0BC2A3E9694F4 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -365,7 +366,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = BP98Z28R86; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", @@ -397,7 +397,6 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = BP98Z28R86; INFOPLIST_FILE = ReactNativeSdkExampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -420,13 +419,12 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0E36B716BFEBCD6726A94808 /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = C54F5D14A47F992CD62ED1F6 /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BP98Z28R86; ENABLE_BITCODE = NO; INFOPLIST_FILE = ReactNativeSdkExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -451,13 +449,12 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F4BEA47FF573D84ED4C085C9 /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = 2417D1EB7566273B17DC1E08 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BP98Z28R86; INFOPLIST_FILE = ReactNativeSdkExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -511,8 +508,10 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CXX = ""; + DEVELOPMENT_TEAM = BP98Z28R86; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; @@ -568,6 +567,7 @@ ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; USE_HERMES = true; }; @@ -606,8 +606,10 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; CXX = ""; + DEVELOPMENT_TEAM = BP98Z28R86; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; @@ -655,6 +657,8 @@ ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/xcshareddata/xcschemes/ReactNativeSdkExample.xcscheme b/example/ios/ReactNativeSdkExample.xcodeproj/xcshareddata/xcschemes/ReactNativeSdkExample.xcscheme index c3bb832ce..77088a682 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/xcshareddata/xcschemes/ReactNativeSdkExample.xcscheme +++ b/example/ios/ReactNativeSdkExample.xcodeproj/xcshareddata/xcschemes/ReactNativeSdkExample.xcscheme @@ -1,6 +1,6 @@