Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notifications renewal [SW-631] #4699

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"date-fns": "^2.30.0",
"ethers": "^6.13.4",
"exponential-backoff": "^3.1.0",
"firebase": "^10.3.1",
"firebase": "^11.1.0",
"fuse.js": "^7.0.0",
"idb-keyval": "^6.2.1",
"js-cookie": "^3.0.1",
Expand Down
33 changes: 26 additions & 7 deletions apps/web/src/components/common/Notifications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ReactElement, SyntheticEvent } from 'react'
import { useCallback, useEffect } from 'react'
import React, { useCallback, useEffect } from 'react'
import groupBy from 'lodash/groupBy'
import { useAppDispatch, useAppSelector } from '@/store'
import type { Notification } from '@/store/notificationsSlice'
import { closeNotification, readNotification, selectNotifications } from '@/store/notificationsSlice'
import type { AlertColor, SnackbarCloseReason } from '@mui/material'
import { Alert, Link, Snackbar, Typography } from '@mui/material'
import { Alert, Box, Link, Snackbar, Typography } from '@mui/material'
import css from './styles.module.css'
import NextLink from 'next/link'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
Expand All @@ -26,20 +26,39 @@ export const NotificationLink = ({
return null
}

const LinkWrapper = ({ children }: React.PropsWithChildren) =>
'href' in link ? (
<NextLink href={link.href} passHref legacyBehavior>
{children}
</NextLink>
) : (
<Box display="flex">{children}</Box>
)

const handleClick = (event: SyntheticEvent) => {
if ('onClick' in link) {
link.onClick()
}
onClick(event)
}

const isExternal =
typeof link.href === 'string' ? !isRelativeUrl(link.href) : !!(link.href.host || link.href.hostname)
'href' in link &&
(typeof link.href === 'string' ? !isRelativeUrl(link.href) : !!(link.href.host || link.href.hostname))

return (
<Track {...OVERVIEW_EVENTS.NOTIFICATION_INTERACTION} label={link.title} as="span">
<NextLink href={link.href} passHref legacyBehavior>
<LinkWrapper>
<Link
className={css.link}
onClick={onClick}
onClick={handleClick}
sx={{ cursor: 'pointer' }}
{...(isExternal && { target: '_blank', rel: 'noopener noreferrer' })}
>
{link.title} <ChevronRightIcon />
{link.title}
<ChevronRightIcon />
</Link>
</NextLink>
</LinkWrapper>
</Track>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics'
import SvgIcon from '@mui/icons-material/ExpandLess'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { useNotificationsRenewal } from '@/components/settings/PushNotifications/hooks/useNotificationsRenewal'

const NOTIFICATION_CENTER_LIMIT = 4

Expand All @@ -38,6 +39,9 @@ const NotificationCenter = (): ReactElement => {
const hasPushNotifications = useHasFeature(FEATURES.PUSH_NOTIFICATIONS)
const dispatch = useAppDispatch()

// This hook is used to show the notification renewal message when the app is opened
useNotificationsRenewal(true)

const notifications = useAppSelector(selectNotifications)
const chronologicalNotifications = useMemo(() => {
// Clone as Redux returns read-only array
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState, type ReactElement } from 'react'
import { Alert, Box, Button, Typography } from '@mui/material'
import useSafeInfo from '@/hooks/useSafeInfo'
import CheckWallet from '@/components/common/CheckWallet'
import { useNotificationsRenewal } from '@/components/settings/PushNotifications/hooks/useNotificationsRenewal'
import { useIsNotificationsRenewalEnabled } from '@/components/settings/PushNotifications/hooks/useNotificationsTokenVersion'

const NotificationRenewal = (): ReactElement => {
const { safe, safeLoaded } = useSafeInfo()
const [isRegistering, setIsRegistering] = useState(false)
const { renewNotifications, needsRenewal, numberChainsForRenewal } = useNotificationsRenewal()
const isNotificationsRenewalEnabled = useIsNotificationsRenewalEnabled()

if (!needsRenewal || !isNotificationsRenewalEnabled) {
// No need to renew any Safe's notifications
return <></>
}

const handeSignClick = async () => {
setIsRegistering(true)
await renewNotifications()
setIsRegistering(false)
}

const message = `We’ve upgraded your notification experience. Sign ${safeLoaded || numberChainsForRenewal < 2 ? 'the message' : `${numberChainsForRenewal} messages`} now to keep receiving important updates seamlessly.`

return (
<>
<Alert severity="warning">
<Typography
variant="body2"
sx={{
fontWeight: 700,
mb: 1,
}}
>
Signature needed
</Typography>
<Typography variant="body2">{message}</Typography>
</Alert>
<Box>
<CheckWallet allowNonOwner checkNetwork={!isRegistering && safe.deployed}>
{(isOk) => (
<Button
variant="contained"
size="small"
sx={{ width: '200px' }}
onClick={handeSignClick}
disabled={!isOk || isRegistering || !safe.deployed}
>
Sign now
</Button>
)}
</CheckWallet>
</Box>
</>
)
}

export default NotificationRenewal
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useNotificationPreferences } from './hooks/useNotificationPreferences'
import { useNotificationRegistrations } from './hooks/useNotificationRegistrations'
import { trackEvent } from '@/services/analytics'
import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications'
import { requestNotificationPermission } from './logic'
import { mergeNotifiableSafes, requestNotificationPermission } from './logic'
import type { NotifiableSafes } from './logic'
import type { PushNotificationPreferences } from '@/services/push-notifications/preferences'
import CheckWallet from '@/components/common/CheckWallet'
Expand All @@ -40,6 +40,7 @@ import useAllOwnedSafes from '@/features/myAccounts/hooks/useAllOwnedSafes'
import useWallet from '@/hooks/wallets/useWallet'
import { selectAllAddedSafes, type AddedSafesState } from '@/store/addedSafesSlice'
import { maybePlural } from '@/utils/formatters'
import { useNotificationsRenewal } from './hooks/useNotificationsRenewal'

// UI logic

Expand Down Expand Up @@ -268,6 +269,8 @@ export const GlobalPushNotifications = (): ReactElement | null => {
const { unregisterDeviceNotifications, unregisterSafeNotifications, registerNotifications } =
useNotificationRegistrations()

const { safesForRenewal } = useNotificationsRenewal()

// Safes selected in the UI
const [selectedSafes, setSelectedSafes] = useState<NotifiableSafes>({})

Expand Down Expand Up @@ -349,7 +352,11 @@ export const GlobalPushNotifications = (): ReactElement | null => {

const registrationPromises: Array<Promise<unknown>> = []

const safesToRegister = _getSafesToRegister(selectedSafes, currentNotifiedSafes)
const newlySelectedSafes = _getSafesToRegister(selectedSafes, currentNotifiedSafes)

// Merge Safes that need to be registered with the ones for which notifications need to be renewed
const safesToRegister = mergeNotifiableSafes(newlySelectedSafes, safesForRenewal)

if (safesToRegister) {
registrationPromises.push(registerNotifications(safesToRegister))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const useNotificationPreferences = (): {
[PushNotificationPrefsKey, PushNotificationPreferences[PushNotificationPrefsKey]][]
>
_deleteManyPreferenceKeys: (keysToDelete: PushNotificationPrefsKey[]) => void
getChainPreferences: (chainId: string) => PushNotificationPreferences[PushNotificationPrefsKey][]
} => {
// State
const uuid = useUuid()
Expand All @@ -72,6 +73,14 @@ export const useNotificationPreferences = (): {
return preferences
}, [preferences])

// Get list of preferences for specified chain
const getChainPreferences = useCallback(
(chainId: string) => {
return Object.values(preferences || {}).filter((pref) => chainId === pref.chainId)
},
[preferences],
)

// idb-keyval stores
const uuidStore = useMemo(() => {
if (typeof indexedDB !== 'undefined') {
Expand Down Expand Up @@ -253,5 +262,6 @@ export const useNotificationPreferences = (): {
deleteAllChainPreferences,
_getAllPreferenceEntries,
_deleteManyPreferenceKeys,
getChainPreferences,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { logError } from '@/services/exceptions'
import ErrorCodes from '@/services/exceptions/ErrorCodes'
import useWallet from '@/hooks/wallets/useWallet'
import type { NotifiableSafes } from '../logic'
import { NotificationsTokenVersion } from '@/services/push-notifications/preferences'
import { useNotificationsTokenVersion } from './useNotificationsTokenVersion'

const registrationFlow = async (registrationFn: Promise<unknown>, callback: () => void): Promise<boolean> => {
let success = false
Expand Down Expand Up @@ -40,6 +42,7 @@ export const useNotificationRegistrations = (): {
const dispatch = useAppDispatch()
const wallet = useWallet()

const { setTokenVersion } = useNotificationsTokenVersion()
const { uuid, createPreferences, deletePreferences, deleteAllChainPreferences } = useNotificationPreferences()

const registerNotifications = async (safesToRegister: NotifiableSafes) => {
Expand All @@ -65,6 +68,9 @@ export const useNotificationRegistrations = (): {
0,
)

// Set the token version to V2 to indicate that the user has registered their token for the new notification service
setTokenVersion(NotificationsTokenVersion.V2, safesToRegister)

trackEvent({
...PUSH_NOTIFICATION_EVENTS.REGISTER_SAFES,
label: totalRegistered,
Expand Down
Loading
Loading