diff --git a/src/embedded/components/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification.tsx
deleted file mode 100644
index 686ea01e6..000000000
--- a/src/embedded/components/IterableEmbeddedNotification.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-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/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts
new file mode 100644
index 000000000..923df66fc
--- /dev/null
+++ 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.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' })
+ );
+ });
+ });
+});
diff --git a/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx
new file mode 100644
index 000000000..f0909cfc5
--- /dev/null
+++ b/src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx
@@ -0,0 +1,96 @@
+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) => {
+ const { parsedStyles, handleButtonClick, handleMessageClick } =
+ useEmbeddedView(IterableEmbeddedViewType.Notification, {
+ message,
+ config,
+ onButtonClick,
+ onMessageClick,
+ });
+
+ const buttons = message.elements?.buttons ?? [];
+
+ return (
+ 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}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+};
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';
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;
};
diff --git a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts
index 1ed04d3ee..cfbd9fc4f 100644
--- a/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts
+++ b/src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts
@@ -1,7 +1,8 @@
-import { useMemo } from 'react';
-
+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 './getMedia';
import { getStyles } from './getStyles';
@@ -13,18 +14,18 @@ import { getStyles } from './getStyles';
* @returns The embedded view.
*
* @example
- * const \{ media, parsedStyles \} = useEmbeddedView(IterableEmbeddedViewType.Notification, \{
+ * const { handleButtonClick, handleMessageClick, media, parsedStyles } = useEmbeddedView(IterableEmbeddedViewType.Notification, {
* message,
* config,
* onButtonClick,
* onMessageClick,
- * \});
+ * });
*
* return (
*
- * \{media.url\}
- * \{media.caption\}
- * \{parsedStyles.backgroundColor\}
+ * {media.url}
+ * {media.caption}
+ * {parsedStyles.backgroundColor}
*
* );
*/
@@ -33,8 +34,10 @@ export const useEmbeddedView = (
viewType: IterableEmbeddedViewType,
/** The props for the embedded view. */
{
- config,
message,
+ config,
+ onButtonClick = () => {},
+ onMessageClick = () => {},
}: IterableEmbeddedComponentProps
) => {
const parsedStyles = useMemo(() => {
@@ -44,8 +47,28 @@ export const useEmbeddedView = (
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 {
- parsedStyles,
+ handleButtonClick,
+ handleMessageClick,
media,
+ parsedStyles,
};
};
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/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;
}