Skip to content

Commit 1e49347

Browse files
committed
feat: introduce IterableEmbeddedNotification component with styling and visibility management
1 parent 8b6f82c commit 1e49347

18 files changed

Lines changed: 562 additions & 25 deletions

example/src/components/Embedded/Embedded.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const Embedded = () => {
1616
IterableEmbeddedMessage[]
1717
>([]);
1818
const [selectedViewType, setSelectedViewType] =
19-
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Banner);
19+
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Notification);
2020

2121
const syncEmbeddedMessages = useCallback(() => {
2222
Iterable.embeddedManager.syncMessages();

src/core/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './useAppStateListener';
22
export * from './useDeviceOrientation';
3+
export * from './useComponentVisibility';
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
View,
3+
Dimensions,
4+
AppState,
5+
type LayoutChangeEvent,
6+
} from 'react-native';
7+
import { useRef, useState, useCallback, useEffect } from 'react';
8+
9+
interface UseVisibilityOptions {
10+
threshold?: number; // Percentage of component that must be visible (0-1)
11+
checkOnAppState?: boolean; // Whether to check app state (active/background)
12+
checkInterval?: number; // How often to check visibility in ms (0 = only on layout changes)
13+
enablePeriodicCheck?: boolean; // Whether to enable periodic checking for navigation changes
14+
}
15+
16+
interface LayoutInfo {
17+
x: number;
18+
y: number;
19+
width: number;
20+
height: number;
21+
}
22+
23+
export const useComponentVisibility = (options: UseVisibilityOptions = {}) => {
24+
const {
25+
threshold = 0.1,
26+
checkOnAppState = true,
27+
checkInterval = 0, // Default to only check on layout changes
28+
enablePeriodicCheck = true, // Enable periodic checking by default for navigation
29+
} = options;
30+
31+
const [isVisible, setIsVisible] = useState(false);
32+
const [appState, setAppState] = useState(AppState.currentState);
33+
const componentRef = useRef<View>(null);
34+
const [layout, setLayout] = useState<LayoutInfo>({
35+
x: 0,
36+
y: 0,
37+
width: 0,
38+
height: 0,
39+
});
40+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
41+
42+
// Handle layout changes
43+
const handleLayout = useCallback((event: LayoutChangeEvent) => {
44+
const { x, y, width, height } = event.nativeEvent.layout;
45+
setLayout({ x, y, width, height });
46+
}, []);
47+
48+
// Check if component is visible on screen using measure
49+
const checkVisibility = useCallback((): Promise<boolean> => {
50+
if (!componentRef.current || layout.width === 0 || layout.height === 0) {
51+
return Promise.resolve(false);
52+
}
53+
54+
return new Promise<boolean>((resolve) => {
55+
componentRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
56+
const screenHeight = Dimensions.get('window').height;
57+
const screenWidth = Dimensions.get('window').width;
58+
59+
// Calculate visible area using page coordinates
60+
const visibleTop = Math.max(0, pageY);
61+
const visibleBottom = Math.min(screenHeight, pageY + height);
62+
const visibleLeft = Math.max(0, pageX);
63+
const visibleRight = Math.min(screenWidth, pageX + width);
64+
65+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
66+
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
67+
68+
const visibleArea = visibleHeight * visibleWidth;
69+
const totalArea = height * width;
70+
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;
71+
72+
resolve(visibilityRatio >= threshold);
73+
});
74+
}).catch(() => {
75+
// Fallback to layout-based calculation if measure fails
76+
const screenHeight = Dimensions.get('window').height;
77+
const screenWidth = Dimensions.get('window').width;
78+
79+
const visibleTop = Math.max(0, layout.y);
80+
const visibleBottom = Math.min(screenHeight, layout.y + layout.height);
81+
const visibleLeft = Math.max(0, layout.x);
82+
const visibleRight = Math.min(screenWidth, layout.x + layout.width);
83+
84+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
85+
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
86+
87+
const visibleArea = visibleHeight * visibleWidth;
88+
const totalArea = layout.height * layout.width;
89+
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;
90+
91+
return visibilityRatio >= threshold;
92+
});
93+
}, [layout, threshold]);
94+
95+
// Update visibility state
96+
const updateVisibility = useCallback(async () => {
97+
const isComponentVisible = await checkVisibility();
98+
const isAppActive = !checkOnAppState || appState === 'active';
99+
const newVisibility = isComponentVisible && isAppActive;
100+
101+
setIsVisible(newVisibility);
102+
}, [checkVisibility, appState, checkOnAppState]);
103+
104+
// Update visibility when layout or app state changes
105+
useEffect(() => {
106+
updateVisibility();
107+
}, [updateVisibility]);
108+
109+
// Set up periodic checking for navigation changes
110+
useEffect(() => {
111+
const interval =
112+
checkInterval > 0 ? checkInterval : enablePeriodicCheck ? 500 : 0;
113+
114+
if (interval > 0) {
115+
intervalRef.current = setInterval(updateVisibility, interval);
116+
return () => {
117+
if (intervalRef.current) {
118+
clearInterval(intervalRef.current);
119+
}
120+
};
121+
}
122+
return undefined;
123+
}, [checkInterval, enablePeriodicCheck, updateVisibility]);
124+
125+
// Listen to app state changes
126+
useEffect(() => {
127+
if (!checkOnAppState) return;
128+
129+
const handleAppStateChange = (nextAppState: string) => {
130+
setAppState(nextAppState as typeof AppState.currentState);
131+
};
132+
133+
const subscription = AppState.addEventListener(
134+
'change',
135+
handleAppStateChange
136+
);
137+
return () => subscription?.remove();
138+
}, [checkOnAppState]);
139+
140+
// Clean up interval on unmount
141+
useEffect(() => {
142+
return () => {
143+
if (intervalRef.current) {
144+
clearInterval(intervalRef.current);
145+
}
146+
};
147+
}, []);
148+
149+
return {
150+
isVisible,
151+
componentRef,
152+
handleLayout,
153+
appState,
154+
layout,
155+
};
156+
};

src/embedded/components/IterableEmbeddedNotification.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
export const styles = StyleSheet.create({
4+
body: {
5+
alignSelf: 'stretch',
6+
fontSize: 14,
7+
fontWeight: '400',
8+
lineHeight: 20,
9+
},
10+
bodyContainer: {
11+
display: 'flex',
12+
flexDirection: 'column',
13+
flexGrow: 1,
14+
flexShrink: 1,
15+
gap: 4,
16+
width: '100%',
17+
},
18+
button: {
19+
borderRadius: 32,
20+
gap: 8,
21+
paddingHorizontal: 12,
22+
paddingVertical: 8,
23+
},
24+
buttonContainer: {
25+
alignItems: 'flex-start',
26+
alignSelf: 'stretch',
27+
display: 'flex',
28+
flexDirection: 'row',
29+
gap: 12,
30+
width: '100%',
31+
},
32+
buttonText: {
33+
fontSize: 14,
34+
fontWeight: '700',
35+
lineHeight: 20,
36+
},
37+
container: {
38+
alignItems: 'flex-start',
39+
borderStyle: 'solid',
40+
boxShadow:
41+
'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)',
42+
display: 'flex',
43+
flexDirection: 'column',
44+
gap: 8,
45+
justifyContent: 'center',
46+
padding: 16,
47+
width: '100%',
48+
},
49+
title: {
50+
fontSize: 16,
51+
fontWeight: '700',
52+
lineHeight: 24,
53+
},
54+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
Text,
3+
TouchableOpacity,
4+
View,
5+
type TextStyle,
6+
type ViewStyle,
7+
Pressable,
8+
} from 'react-native';
9+
10+
import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType';
11+
import { useEmbeddedView } from '../../hooks/useEmbeddedView';
12+
import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps';
13+
import { styles } from './IterableEmbeddedNotification.styles';
14+
15+
export const IterableEmbeddedNotification = ({
16+
config,
17+
message,
18+
onButtonClick = () => {},
19+
onMessageClick = () => {},
20+
}: IterableEmbeddedComponentProps) => {
21+
const { parsedStyles, handleButtonClick, handleMessageClick } =
22+
useEmbeddedView(IterableEmbeddedViewType.Notification, {
23+
message,
24+
config,
25+
onButtonClick,
26+
onMessageClick,
27+
});
28+
29+
const buttons = message.elements?.buttons ?? [];
30+
31+
return (
32+
<Pressable onPress={() => handleMessageClick()}>
33+
<View
34+
style={[
35+
styles.container,
36+
{
37+
backgroundColor: parsedStyles.backgroundColor,
38+
borderColor: parsedStyles.borderColor,
39+
borderRadius: parsedStyles.borderCornerRadius,
40+
borderWidth: parsedStyles.borderWidth,
41+
} as ViewStyle,
42+
]}
43+
>
44+
{}
45+
<View style={styles.bodyContainer}>
46+
<Text
47+
style={[
48+
styles.title,
49+
{ color: parsedStyles.titleTextColor } as TextStyle,
50+
]}
51+
>
52+
{message.elements?.title}
53+
</Text>
54+
<Text
55+
style={[
56+
styles.body,
57+
{ color: parsedStyles.bodyTextColor } as TextStyle,
58+
]}
59+
>
60+
{message.elements?.body}
61+
</Text>
62+
</View>
63+
{buttons.length > 0 && (
64+
<View style={styles.buttonContainer}>
65+
{buttons.map((button, index) => {
66+
const backgroundColor =
67+
index === 0
68+
? parsedStyles.primaryBtnBackgroundColor
69+
: parsedStyles.secondaryBtnBackgroundColor;
70+
const textColor =
71+
index === 0
72+
? parsedStyles.primaryBtnTextColor
73+
: parsedStyles.secondaryBtnTextColor;
74+
return (
75+
<TouchableOpacity
76+
style={[styles.button, { backgroundColor } as ViewStyle]}
77+
onPress={() => handleButtonClick(button)}
78+
key={button.id}
79+
>
80+
<Text
81+
style={[
82+
styles.buttonText,
83+
{ color: textColor } as TextStyle,
84+
]}
85+
>
86+
{button.title}
87+
</Text>
88+
</TouchableOpacity>
89+
);
90+
})}
91+
</View>
92+
)}
93+
</View>
94+
</Pressable>
95+
);
96+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './IterableEmbeddedNotification';

src/embedded/components/IterableEmbeddedView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType';
44

55
import { IterableEmbeddedBanner } from './IterableEmbeddedBanner';
66
import { IterableEmbeddedCard } from './IterableEmbeddedCard';
7-
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification';
7+
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification/IterableEmbeddedNotification';
88
import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps';
99

1010
/**

src/embedded/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from './IterableEmbeddedBanner';
22
export * from './IterableEmbeddedCard';
3-
export * from './IterableEmbeddedNotification';
3+
export * from './IterableEmbeddedNotification/IterableEmbeddedNotification';
44
export * from './IterableEmbeddedView';

0 commit comments

Comments
 (0)