Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/src/components/Embedded/Embedded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const Embedded = () => {
IterableEmbeddedMessage[]
>([]);
const [selectedViewType, setSelectedViewType] =
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Banner);
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Notification);

const syncEmbeddedMessages = useCallback(() => {
Iterable.embeddedManager.syncMessages();
Expand Down
1 change: 1 addition & 0 deletions src/core/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useAppStateListener';
export * from './useDeviceOrientation';
export * from './useComponentVisibility';
156 changes: 156 additions & 0 deletions src/core/hooks/useComponentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
View,
Dimensions,
AppState,
type LayoutChangeEvent,
} from 'react-native';
import { useRef, useState, useCallback, useEffect } from 'react';

interface UseVisibilityOptions {
threshold?: number; // Percentage of component that must be visible (0-1)
checkOnAppState?: boolean; // Whether to check app state (active/background)
checkInterval?: number; // How often to check visibility in ms (0 = only on layout changes)
enablePeriodicCheck?: boolean; // Whether to enable periodic checking for navigation changes
}

interface LayoutInfo {
x: number;
y: number;
width: number;
height: number;
}

export const useComponentVisibility = (options: UseVisibilityOptions = {}) => {
const {
threshold = 0.1,
checkOnAppState = true,
checkInterval = 0, // Default to only check on layout changes
enablePeriodicCheck = true, // Enable periodic checking by default for navigation
} = options;

const [isVisible, setIsVisible] = useState(false);
const [appState, setAppState] = useState(AppState.currentState);
const componentRef = useRef<View>(null);
const [layout, setLayout] = useState<LayoutInfo>({
x: 0,
y: 0,
width: 0,
height: 0,
});
const intervalRef = useRef<NodeJS.Timeout | null>(null);

// Handle layout changes
const handleLayout = useCallback((event: LayoutChangeEvent) => {
const { x, y, width, height } = event.nativeEvent.layout;
setLayout({ x, y, width, height });
}, []);

// Check if component is visible on screen using measure
const checkVisibility = useCallback((): Promise<boolean> => {
if (!componentRef.current || layout.width === 0 || layout.height === 0) {
return Promise.resolve(false);
}

return new Promise<boolean>((resolve) => {
componentRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
const screenHeight = Dimensions.get('window').height;
const screenWidth = Dimensions.get('window').width;

// Calculate visible area using page coordinates
const visibleTop = Math.max(0, pageY);
const visibleBottom = Math.min(screenHeight, pageY + height);
const visibleLeft = Math.max(0, pageX);
const visibleRight = Math.min(screenWidth, pageX + width);

const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);

const visibleArea = visibleHeight * visibleWidth;
const totalArea = height * width;
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;

resolve(visibilityRatio >= threshold);
});
}).catch(() => {
// Fallback to layout-based calculation if measure fails
const screenHeight = Dimensions.get('window').height;
const screenWidth = Dimensions.get('window').width;

const visibleTop = Math.max(0, layout.y);
const visibleBottom = Math.min(screenHeight, layout.y + layout.height);
const visibleLeft = Math.max(0, layout.x);
const visibleRight = Math.min(screenWidth, layout.x + layout.width);

const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);

const visibleArea = visibleHeight * visibleWidth;
const totalArea = layout.height * layout.width;
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;

return visibilityRatio >= threshold;
});
}, [layout, threshold]);

// Update visibility state
const updateVisibility = useCallback(async () => {
const isComponentVisible = await checkVisibility();
const isAppActive = !checkOnAppState || appState === 'active';
const newVisibility = isComponentVisible && isAppActive;

setIsVisible(newVisibility);
}, [checkVisibility, appState, checkOnAppState]);

// Update visibility when layout or app state changes
useEffect(() => {
updateVisibility();
}, [updateVisibility]);

// Set up periodic checking for navigation changes
useEffect(() => {
const interval =
checkInterval > 0 ? checkInterval : enablePeriodicCheck ? 500 : 0;

if (interval > 0) {
intervalRef.current = setInterval(updateVisibility, interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}
return undefined;
}, [checkInterval, enablePeriodicCheck, updateVisibility]);

// Listen to app state changes
useEffect(() => {
if (!checkOnAppState) return;

const handleAppStateChange = (nextAppState: string) => {
setAppState(nextAppState as typeof AppState.currentState);
};

const subscription = AppState.addEventListener(
'change',
handleAppStateChange
);
return () => subscription?.remove();
}, [checkOnAppState]);

// Clean up interval on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);

return {
isVisible,
componentRef,
handleLayout,
appState,
layout,
};
Comment on lines +23 to +155
Copy link
Copy Markdown

@qltysh qltysh Bot Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Function with many returns (count = 8): useComponentVisibility [qlty:return-statements]


2. Function with high complexity (count = 36): useComponentVisibility [qlty:function-complexity]

};
22 changes: 0 additions & 22 deletions src/embedded/components/IterableEmbeddedNotification.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable onPress={() => handleMessageClick()}>
<View
style={[
styles.container,
{
backgroundColor: parsedStyles.backgroundColor,
borderColor: parsedStyles.borderColor,
borderRadius: parsedStyles.borderCornerRadius,
borderWidth: parsedStyles.borderWidth,
} as ViewStyle,
]}
>
{}
<View style={styles.bodyContainer}>
<Text
style={[
styles.title,
{ color: parsedStyles.titleTextColor } as TextStyle,
]}
>
{message.elements?.title}
</Text>
<Text
style={[
styles.body,
{ color: parsedStyles.bodyTextColor } as TextStyle,
]}
>
{message.elements?.body}
</Text>
</View>
{buttons.length > 0 && (
<View style={styles.buttonContainer}>
{buttons.map((button, index) => {
const backgroundColor =
index === 0
? parsedStyles.primaryBtnBackgroundColor
: parsedStyles.secondaryBtnBackgroundColor;
const textColor =
index === 0
? parsedStyles.primaryBtnTextColor
: parsedStyles.secondaryBtnTextColor;
return (
<TouchableOpacity
style={[styles.button, { backgroundColor } as ViewStyle]}
onPress={() => handleButtonClick(button)}
key={button.id}
>
<Text
style={[
styles.buttonText,
{ color: textColor } as TextStyle,
]}
>
{button.title}
</Text>
</TouchableOpacity>
);
})}
</View>
)}
</View>
</Pressable>
);
Comment on lines +15 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 7): IterableEmbeddedNotification [qlty:function-complexity]

};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './IterableEmbeddedNotification';
2 changes: 1 addition & 1 deletion src/embedded/components/IterableEmbeddedView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType';

import { IterableEmbeddedBanner } from './IterableEmbeddedBanner';
import { IterableEmbeddedCard } from './IterableEmbeddedCard';
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification';
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification/IterableEmbeddedNotification';
import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/embedded/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './IterableEmbeddedBanner';
export * from './IterableEmbeddedCard';
export * from './IterableEmbeddedNotification';
export * from './IterableEmbeddedNotification/IterableEmbeddedNotification';
export * from './IterableEmbeddedView';
Loading