import { NativeStackScreenProps } from '@react-navigation/native-stack' import CleverTap from 'clevertap-react-native' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LayoutChangeEvent, StyleSheet, Text, View, ViewToken } from 'react-native' import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import AppAnalytics from 'src/analytics/AppAnalytics' import { HomeEvents } from 'src/analytics/Events' import { openUrl } from 'src/app/actions' import { CallToAction } from 'src/components/CallToActionsBar' import SimpleMessagingCard from 'src/components/SimpleMessagingCard' import { CLEVERTAP_PRIORITY, useSimpleActions } from 'src/home/NotificationBox' import { cleverTapInboxMessagesSelector } from 'src/home/selectors' import { Notification, NotificationBannerCTATypes, NotificationType } from 'src/home/types' import ThumbsUpIllustration from 'src/icons/ThumbsUpIllustration' import { Screens } from 'src/navigator/Screens' import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader' import { StackParamList } from 'src/navigator/types' import { useDispatch, useSelector } from 'src/redux/hooks' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' type NotificationsProps = NativeStackScreenProps function useCleverTapNotifications() { const { t } = useTranslation() const dispatch = useDispatch() const messages = useSelector(cleverTapInboxMessagesSelector) return useMemo(() => { const notifications: Notification[] = [] for (const { messageId, header, text, icon, ctaText, ctaUrl, priority, openInExternalBrowser, } of messages) { const notificationId = `${NotificationType.clevertap_notification}/${messageId}` const callToActions: CallToAction[] = [ { text: ctaText, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.clevertap_notification, selectedAction: NotificationBannerCTATypes.accept, notificationId, notificationPositionInList: params?.index, }) CleverTap.pushInboxNotificationClickedEventForId(messageId) const isSecureOrigin = true dispatch(openUrl(ctaUrl, openInExternalBrowser, isSecureOrigin)) }, }, { text: t('dismiss'), isSecondary: true, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.clevertap_notification, selectedAction: NotificationBannerCTATypes.decline, notificationId, notificationPositionInList: params?.index, }) CleverTap.deleteInboxMessageForId(messageId) }, }, ] notifications.push({ renderElement: (params?: { index?: number }) => ( ), priority: priority || CLEVERTAP_PRIORITY, showOnHomeScreen: false, id: notificationId, type: NotificationType.clevertap_notification, onView: () => { CleverTap.pushInboxNotificationViewedEventForId(messageId) CleverTap.markReadInboxMessageForId(messageId) }, }) } return notifications }, [messages]) } export function useNotifications() { const notifications: Notification[] = [] const simpleActions = useSimpleActions() notifications.push( ...simpleActions.map((notification) => ({ renderElement: (params?: { index?: number }) => ( ), priority: notification.priority, showOnHomeScreen: notification.showOnHomeScreen, id: notification.id, type: notification.type, })) ) const cleverTapNotifications = useCleverTapNotifications() notifications.push(...cleverTapNotifications) return ( notifications .sort((n1, n2) => n2.priority - n1.priority) // Hide notifications that should only be shown on the home screen .filter((n) => !n.showOnHomeScreen) ) } export default function Notifications({ navigation }: NotificationsProps) { const safeAreaInsets = useSafeAreaInsets() const { t } = useTranslation() const title = t('notifications') const notifications = useNotifications() // Changing onViewableItemsChanged on the fly is not supported. // This is a workaround to provide handleViewableItemsChanged with // actual notifications array while keeping its dependecy list empty. const notificationsRef = useRef([]) useEffect(() => { notificationsRef.current = notifications }, [notifications]) const seenNotifications = useRef(new Set()) useEffect(() => { AppAnalytics.track(HomeEvents.notification_center_opened, { notificationsCount: notifications.length, }) }, []) const viewabilityConfig = useRef({ itemVisiblePercentThreshold: 100, minimumViewTime: 500, }) const scrollPosition = useSharedValue(0) const [headerYPosition, setHeaderYPosition] = useState(0) const [headerHeight, setHeaderHeight] = useState(0) useScrollAwareHeader({ navigation, title, scrollPosition, startFadeInPosition: headerYPosition, animationDistance: headerHeight, }) const handleScroll = useAnimatedScrollHandler((event) => { scrollPosition.value = event.contentOffset.y }) const handleViewableItemsChanged = useCallback( ({ viewableItems }: { viewableItems: ViewToken[] }) => { viewableItems.forEach(({ item }: { item: Notification }) => { if (!seenNotifications.current.has(item.id)) { seenNotifications.current.add(item.id) AppAnalytics.track(HomeEvents.notification_impression, { notificationId: item.id, notificationType: item.type, notificationPositionInList: notificationsRef.current.findIndex( ({ id }) => id === item.id ), }) item.onView?.() } }) }, [] ) const handleMeasureHeaderHeight = (event: LayoutChangeEvent) => { setHeaderYPosition(event.nativeEvent.layout.y) setHeaderHeight(event.nativeEvent.layout.height) } const renderHeader = () => ( {title} ) const renderEmptyState = () => ( {t('noNotificationsPlaceholder')} ) const renderItemSeparator = () => const renderItem = ({ item, index }: { item: Notification; index: number }) => { return ( {item.renderElement({ index })} ) } const keyExtractor = (item: Notification) => item.id const contentContainerStyle = { paddingBottom: safeAreaInsets.bottom + Spacing.Regular16 } return ( ) } const styles = StyleSheet.create({ container: { flex: 1, }, title: { ...typeScale.titleMedium, margin: Spacing.Thick24, marginTop: Spacing.Smallest8, }, listItem: { paddingHorizontal: Spacing.Thick24, }, itemSeparator: { height: Spacing.Thick24, }, emptyStateContainer: { marginTop: 40, alignItems: 'center', }, emptyStateLabel: { ...typeScale.labelSemiBoldMedium, marginTop: Spacing.Regular16, }, })