Skip to content

Conversation

@lposen
Copy link
Contributor

@lposen lposen commented Oct 14, 2025

🔹 JIRA Ticket(s) if any

✏️ Description

Please provide a brief description of what this pull request does.

@github-actions
Copy link

Lines Statements Branches Functions
Coverage: 41%
41.92% (314/749) 17.86% (57/319) 39.38% (102/259)

@qltysh
Copy link

qltysh bot commented Oct 14, 2025

Diff Coverage: The code coverage on the diff in this pull request is 15.6%.

Total Coverage: This PR will decrease coverage by 5.3%.

File Coverage Changes
Path File Coverage Δ Indirect
src/core/hooks/useComponentVisibility.ts 1.5
src/embedded/components/IterableEmbeddedNotification.tsx -20.0
src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.styles.ts 100.0
src/embedded/components/IterableEmbeddedNotification/IterableEmbeddedNotification.tsx 11.1
src/embedded/components/IterableEmbeddedNotification/index.ts 100.0
src/embedded/constants/embeddedViewDefaults.ts 100.0
src/embedded/constants/index.ts 100.0
src/embedded/hooks/index.ts 100.0
src/embedded/hooks/useEmbeddedView.ts 3.9
src/embedded/utils/getDefaultStyle.ts 20.0
src/embedded/utils/getMedia.ts 14.3
src/embedded/utils/getStyles.ts 50.0
src/embedded/utils/index.ts 100.0
🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

This is from Qlty Cloud, the successor to Code Climate Quality. Learn more.

@qltysh
Copy link

qltysh bot commented Oct 14, 2025

4 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 36): useComponentVisibility 3
qlty Structure Function with many returns (count = 8): useComponentVisibility 1

This is from Qlty Cloud, the successor to Code Climate Quality. Learn more.

Comment on lines +23 to +155
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,
};
Copy link

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 high complexity (count = 36): useComponentVisibility [qlty:function-complexity]


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

Comment on lines +15 to +95
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>
);
Copy link

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]

Comment on lines +11 to +90
export const useEmbeddedView = (
viewType: IterableEmbeddedViewType,
{
message,
config,
onButtonClick = () => {},
onMessageClick = () => {},
}: IterableEmbeddedComponentProps
) => {
const appVisibility = useAppStateListener();
const { isVisible, componentRef, handleLayout } = useComponentVisibility({
threshold: 0.1, // Component is considered visible if 10% is on screen
checkOnAppState: true, // Consider app state (active/background)
enablePeriodicCheck: true, // Enable periodic checking for navigation changes
checkInterval: 500, // Check every 500ms for navigation changes
});

const parsedStyles = useMemo(() => {
return getStyles(viewType, config);
}, [viewType, config]);
const media = useMemo(() => {
return getMedia(viewType, message);
}, [viewType, message]);

const [lastState, setLastState] = useState('initial');

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]);

useEffect(() => {
if (appVisibility !== lastState) {
setLastState(appVisibility);
if (appVisibility === 'active') {
// App is active, start the session
// TODO: figure out how to only do this once, even if there are multiple embedded views
Iterable.embeddedManager.startSession();
} else if (
appVisibility === 'background' ||
appVisibility === 'inactive'
) {
// App is background or inactive, end the session
// TODO: figure out how to only do this once, even if there are multiple embedded views
Iterable.embeddedManager.endSession();
}
}
}, [appVisibility, lastState]);

useEffect(() => {
if (isVisible) {
Iterable.embeddedManager.startImpression(
message.metadata.messageId,
message.metadata.placementId
);
} else {
Iterable.embeddedManager.pauseImpression(message.metadata.messageId);
}
}, [isVisible, message.metadata.messageId, message.metadata.placementId]);

return {
componentRef,
handleButtonClick,
handleLayout,
handleMessageClick,
media,
parsedStyles,
};
Copy link

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 = 13): useEmbeddedView [qlty:function-complexity]

@lposen lposen added the embedded Issues/PRs related to Embedded Messages label Oct 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

embedded Issues/PRs related to Embedded Messages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants