import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { NativeScrollEvent, ScrollView, StyleSheet, View } from 'react-native' import { dismissGetVerified, dismissGoldEducation } from 'src/account/actions' import { celoEducationCompletedSelector, cloudBackupCompletedSelector } from 'src/account/selectors' import AppAnalytics from 'src/analytics/AppAnalytics' import { HomeEvents } from 'src/analytics/Events' import { ScrollDirection } from 'src/analytics/types' import { openUrl } from 'src/app/actions' import { phoneNumberVerifiedSelector } from 'src/app/selectors' import Pagination from 'src/components/Pagination' import SimpleMessagingCard, { Props as SimpleMessagingCardProps, } from 'src/components/SimpleMessagingCard' import { dismissNotification } from 'src/home/actions' import { DEFAULT_PRIORITY } from 'src/home/reducers' import { getExtraNotifications } from 'src/home/selectors' import { Notification, NotificationBannerCTATypes, NotificationType } from 'src/home/types' import KeylessBackup from 'src/icons/KeylessBackup' import GuideKeyIcon from 'src/images/GuideKeyIcon' import { getVerified, learnCelo } from 'src/images/Images' import { ensurePincode, navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { useDispatch, useSelector } from 'src/redux/hooks' import variables from 'src/styles/variables' import Logger from 'src/utils/Logger' import { getContentForCurrentLang } from 'src/utils/contentTranslations' import { ONBOARDING_FEATURES_ENABLED } from 'src/config' import { ToggleableOnboardingFeatures } from 'src/onboarding/types' const TAG = 'NotificationBox' // Priority of static notifications const BACKUP_PRIORITY = 1000 const VERIFICATION_PRIORITY = 100 export const CLEVERTAP_PRIORITY = 500 const CELO_EDUCATION_PRIORITY = 10 interface SimpleAction extends SimpleMessagingCardProps { id: string priority: number showOnHomeScreen?: boolean type: NotificationType } export function useSimpleActions() { const { backupCompleted, dismissedGetVerified, dismissedGoldEducation } = useSelector( (state) => state.account ) const phoneNumberVerified = useSelector(phoneNumberVerifiedSelector) const celoEducationCompleted = useSelector(celoEducationCompletedSelector) const extraNotifications = useSelector(getExtraNotifications) const { t } = useTranslation() const dispatch = useDispatch() const showKeylessBackup = ONBOARDING_FEATURES_ENABLED[ToggleableOnboardingFeatures.CloudBackup] const cloudBackupCompleted = useSelector(cloudBackupCompletedSelector) const actions: SimpleAction[] = [] if (!backupCompleted && !cloudBackupCompleted) { if (showKeylessBackup) { actions.push({ id: NotificationType.keyless_backup_prompt, type: NotificationType.keyless_backup_prompt, text: t('keylessBackupNotification'), icon: , priority: BACKUP_PRIORITY, testID: 'KeylessBackupNotification', callToActions: [ { text: t('keylessBackupCTA'), onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.keyless_backup_prompt, selectedAction: NotificationBannerCTATypes.accept, notificationId: NotificationType.keyless_backup_prompt, notificationPositionInList: params?.index, }) ensurePincode() .then((pinIsCorrect) => { if (pinIsCorrect) { navigate(Screens.WalletSecurityPrimer) } }) .catch((error) => { Logger.error(`${TAG}@keylessBackupNotification`, 'PIN ensure error', error) }) }, }, ], }) } else { actions.push({ id: NotificationType.backup_prompt, type: NotificationType.backup_prompt, text: t('backupKeyNotification2'), icon: , priority: BACKUP_PRIORITY, testID: 'BackupKeyNotification', callToActions: [ { text: t('backupKeyCTA'), onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.backup_prompt, selectedAction: NotificationBannerCTATypes.accept, notificationId: NotificationType.backup_prompt, notificationPositionInList: params?.index, }) ensurePincode() .then((pinIsCorrect) => { if (pinIsCorrect) { navigate(Screens.BackupIntroduction) } }) .catch((error) => { Logger.error(`${TAG}@backupNotification`, 'PIN ensure error', error) }) }, }, ], }) } } if (!dismissedGetVerified && !phoneNumberVerified) { actions.push({ id: NotificationType.verification_prompt, type: NotificationType.verification_prompt, text: t('notification.body'), icon: getVerified, priority: VERIFICATION_PRIORITY, callToActions: [ { text: t('notification.cta'), onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.verification_prompt, selectedAction: NotificationBannerCTATypes.accept, notificationId: NotificationType.verification_prompt, notificationPositionInList: params?.index, }) navigate(Screens.VerificationStartScreen, { hasOnboarded: true }) }, }, { text: t('dismiss'), isSecondary: true, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.verification_prompt, selectedAction: NotificationBannerCTATypes.decline, notificationId: NotificationType.verification_prompt, notificationPositionInList: params?.index, }) dispatch(dismissGetVerified()) }, }, ], }) } for (const [id, notification] of Object.entries(extraNotifications)) { if (!notification) { continue } const texts = getContentForCurrentLang(notification.content) if (!texts) { continue } actions.push({ id, type: NotificationType.remote_notification, text: texts.body, icon: notification.iconUrl ? { uri: notification.iconUrl } : undefined, priority: notification.priority ?? DEFAULT_PRIORITY, showOnHomeScreen: notification.showOnHomeScreen, callToActions: [ { text: texts.cta, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.remote_notification, selectedAction: NotificationBannerCTATypes.remote_notification_cta, notificationId: id, notificationPositionInList: params?.index, }) dispatch(openUrl(notification.ctaUri, notification.openExternal, true)) }, }, { text: texts.dismiss, isSecondary: true, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.remote_notification, selectedAction: NotificationBannerCTATypes.decline, notificationId: id, notificationPositionInList: params?.index, }) dispatch(dismissNotification(id)) }, }, ], }) } if (!dismissedGoldEducation && !celoEducationCompleted) { actions.push({ id: NotificationType.celo_asset_education, type: NotificationType.celo_asset_education, text: t('whatIsGold'), icon: learnCelo, priority: CELO_EDUCATION_PRIORITY, callToActions: [ { text: t('learnMore'), onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.celo_asset_education, selectedAction: NotificationBannerCTATypes.accept, notificationId: NotificationType.celo_asset_education, notificationPositionInList: params?.index, }) navigate(Screens.GoldEducation) }, }, { text: t('dismiss'), isSecondary: true, onPress: (params) => { AppAnalytics.track(HomeEvents.notification_select, { notificationType: NotificationType.celo_asset_education, selectedAction: NotificationBannerCTATypes.decline, notificationId: NotificationType.celo_asset_education, notificationPositionInList: params?.index, }) dispatch(dismissGoldEducation()) }, }, ], }) } return actions } export function useNotifications({ showOnlyHomeScreenNotifications, }: { showOnlyHomeScreenNotifications: boolean }) { const notifications: Notification[] = [] const simpleActions = useSimpleActions() notifications.push( ...simpleActions.map((notification, i) => ({ renderElement: () => ( ), priority: notification.priority, showOnHomeScreen: notification.showOnHomeScreen, id: notification.id, type: notification.type, })) ) return notifications .sort((n1, n2) => n2.priority - n1.priority) .filter((n) => { if (showOnlyHomeScreenNotifications) { return n.showOnHomeScreen } return true }) } interface Props { showOnlyHomeScreenNotifications: boolean } function NotificationBox({ showOnlyHomeScreenNotifications }: Props) { const [currentIndex, setCurrentIndex] = useState(0) // This variable tracks the last scrolled to notification, so that impression // events are not dispatched twice for the same notification const lastViewedIndex = useRef(-1) const notifications = useNotifications({ showOnlyHomeScreenNotifications }) const handleScroll = (event: { nativeEvent: NativeScrollEvent }) => { const nextIndex = Math.round(event.nativeEvent.contentOffset.x / variables.width) if (nextIndex === currentIndex) { return } const direction = nextIndex > currentIndex ? ScrollDirection.next : ScrollDirection.previous AppAnalytics.track(HomeEvents.notification_scroll, { direction }) setCurrentIndex(Math.round(event.nativeEvent.contentOffset.x / variables.width)) } useEffect(() => { if (notifications.length > 0 && lastViewedIndex.current < currentIndex) { AppAnalytics.track(HomeEvents.notification_impression, { notificationId: notifications[currentIndex].id, notificationType: notifications[currentIndex].type, }) lastViewedIndex.current = currentIndex } }, [currentIndex]) if (!notifications.length) { // No notifications, no slider return null } return ( {notifications.map((notification) => ( {notification.renderElement()} ))} ) } const styles = StyleSheet.create({ body: { maxWidth: variables.width, width: variables.width, }, notificationContainer: { width: variables.width - 2 * variables.contentPadding, margin: variables.contentPadding, }, pagination: { paddingBottom: variables.contentPadding, }, }) export default NotificationBox