Skip to content

Commit

Permalink
fix: register for confirmation requests by default
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Aug 31, 2023
1 parent c2e366a commit 6508339
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { requestNotificationPermission } from './logic'
import type { NotifiableSafes } from './logic'
import type { AddedSafesState } from '@/store/addedSafesSlice'
import type { NotificationPreferences } from './hooks/notifications-idb'
import CheckWallet from '@/components/common/CheckWallet'

import css from './styles.module.css'

Expand Down Expand Up @@ -85,6 +86,7 @@ export const GlobalPushNotifications = (): ReactElement | null => {

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

// Current Safes registered for notifications in indexedDB
const currentNotifiedSafes = useMemo(() => {
Expand All @@ -94,6 +96,7 @@ export const GlobalPushNotifications = (): ReactElement | null => {
}
return transformCurrentSubscribedSafes(allPreferences)
}, [getAllPreferences])
const currentNotifiedChains = currentNotifiedSafes ? Object.keys(currentNotifiedSafes) : []

// `currentNotifiedSafes` is initially undefined until indexedDB resolves
useEffect(() => {
Expand All @@ -120,7 +123,7 @@ export const GlobalPushNotifications = (): ReactElement | null => {
}, [notifiableSafes])

const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => {
const hasChain = Object.keys(selectedSafes).includes(chainId)
const hasChain = selectedChains.includes(chainId)
const hasEverySafe = safeAddresses?.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress))
return hasChain && hasEverySafe
})
Expand All @@ -140,6 +143,10 @@ export const GlobalPushNotifications = (): ReactElement | null => {
})
}

const totalSignaturesRequired = selectedChains.filter((chainId) => {
return !currentNotifiedChains.includes(chainId)
}).length

// Whether Safes need to be (un-)registered with the service
const shouldRegisterSelectedSafes = Object.entries(selectedSafes).some(([chainId, safeAddresses]) => {
return safeAddresses.some((safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress))
Expand Down Expand Up @@ -232,9 +239,21 @@ export const GlobalPushNotifications = (): ReactElement | null => {
My Safes ({totalNotifiableSafes})
</Typography>

<Button variant="contained" disabled={!canSave} onClick={onSave}>
Save
</Button>
<div>
{totalSignaturesRequired > 0 && (
<Typography display="inline" mr={1}>
You will have to verify with your signature {totalSignaturesRequired} times
</Typography>
)}

<CheckWallet allowNonOwner>
{(isOk) => (
<Button variant="contained" disabled={!canSave || !isOk} onClick={onSave}>
Save
</Button>
)}
</CheckWallet>
</div>
</Grid>

<Grid item xs={12}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,34 @@ import { useNotificationRegistrations } from '../hooks/useNotificationRegistrati
import { transformAddedSafes } from '../GlobalPushNotifications'
import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications'
import { trackEvent } from '@/services/analytics'
import useSafeInfo from '@/hooks/useSafeInfo'
import CheckWallet from '@/components/common/CheckWallet'

import css from './styles.module.css'

const LS_KEY = 'dismissPushNotifications'
const DISMISS_NOTIFICATION_KEY = 'dismissPushNotifications'

export const PushNotificationsBanner = ({ children }: { children: ReactElement }): ReactElement => {
const [dismissedBanner = false, setDismissedBanner] = useLocalStorage<boolean>(LS_KEY)
const addedSafes = useAppSelector(selectAllAddedSafes)
const totalAddedSafes = useAppSelector(selectTotalAdded)

const { safe } = useSafeInfo()
const { query } = useRouter()
const safe = Array.isArray(query.safe) ? query.safe[0] : query.safe

const [dismissedBannerPerChain = {}, setDismissedBannerPerChain] = useLocalStorage<{
[chainId: string]: boolean
}>(DISMISS_NOTIFICATION_KEY)
const dismissedBanner = !!dismissedBannerPerChain[safe.chainId]

const { registerNotifications } = useNotificationRegistrations()

const dismissBanner = useCallback(() => {
trackEvent(PUSH_NOTIFICATION_EVENTS.DISMISS_BANNER)

setDismissedBanner(true)
}, [setDismissedBanner])
setDismissedBannerPerChain({
...dismissedBannerPerChain,
[safe.chainId]: true,
})
}, [dismissedBannerPerChain, safe.chainId, setDismissedBannerPerChain])

// Click outside to dismiss banner
useEffect(() => {
Expand All @@ -49,11 +57,11 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement }
}
}, [dismissBanner, dismissedBanner])

const onEnableAll = () => {
const onEnableAll = async () => {
trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL)

const safesToRegister = transformAddedSafes(addedSafes)
registerNotifications(safesToRegister)
await registerNotifications(safesToRegister)

dismissBanner()
}
Expand Down Expand Up @@ -82,13 +90,24 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement }
Enable push notifications
</Typography>
<Typography mt={0.5} mb={1.5} variant="body2">
Easily track your Safe Account activity with broswer push notifications.
Get notified about pending signatures, incoming and outgoing transactions and more when Safe{`{Wallet}`}{' '}
is in the background or closed.
</Typography>
<div className={css.buttons}>
{totalAddedSafes > 0 && (
<Button variant="contained" size="small" className={css.button} onClick={onEnableAll}>
Enable all
</Button>
<CheckWallet>
{(isOk) => (
<Button
variant="contained"
size="small"
className={css.button}
onClick={onEnableAll}
disabled={!isOk}
>
Enable all
</Button>
)}
</CheckWallet>
)}
{safe && (
<Link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNoti
[WebhookType.INCOMING_TOKEN]: true,
[WebhookType.OUTGOING_TOKEN]: true,
[WebhookType.MODULE_TRANSACTION]: true,
[WebhookType.CONFIRMATION_REQUEST]: false, // Requires signature
[WebhookType.SAFE_CREATED]: false, // Cannot be registered to predicted address
[WebhookType.CONFIRMATION_REQUEST]: true, // Requires signature
[WebhookType.SAFE_CREATED]: false, // We do not preemptively subscribe to Safes before they are created
}

// ExternalStores are used to keep indexedDB state synced across hook instances
Expand All @@ -44,7 +44,7 @@ export const useNotificationPreferences = (): {
safeAddress: string,
preferences: NotificationPreferences[SafeNotificationKey]['preferences'],
) => void
_createPreferences: (safesToRegister: NotifiableSafes, withConfirmationRequests?: boolean) => void
_createPreferences: (safesToRegister: NotifiableSafes) => void
_deletePreferences: (safesToUnregister: NotifiableSafes) => void
_clearPreferences: () => void
} => {
Expand Down Expand Up @@ -124,10 +124,7 @@ export const useNotificationPreferences = (): {
}, [hydratePreferences])

// Add store entry with default preferences for specified Safe(s)
const createPreferences = (
safesToRegister: { [chain: string]: Array<string> },
withConfirmationRequests?: boolean,
) => {
const createPreferences = (safesToRegister: { [chain: string]: Array<string> }) => {
if (!preferencesStore) {
return
}
Expand All @@ -139,9 +136,7 @@ export const useNotificationPreferences = (): {
const defaultPreferences: NotificationPreferences[SafeNotificationKey] = {
chainId,
safeAddress,
preferences: withConfirmationRequests
? { ..._DEFAULT_NOTIFICATION_PREFERENCES, [WebhookType.CONFIRMATION_REQUEST]: true }
: _DEFAULT_NOTIFICATION_PREFERENCES,
preferences: _DEFAULT_NOTIFICATION_PREFERENCES,
}

return [key, defaultPreferences]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif
import { getRegisterDevicePayload } from '../logic'
import type { NotifiableSafes } from '../logic'

const registrationFlow = async (registrationFn: Promise<void>, callback: () => void) => {
let success = false

try {
const response = await registrationFn

// Gateway will return 200 with an empty payload if the device was (un-)registered successfully
// @see https://github.com/safe-global/safe-client-gateway-nest/blob/27b6b3846b4ecbf938cdf5d0595ca464c10e556b/src/routes/notifications/notifications.service.ts#L29
success = response == null
} catch (e) {
console.error('(Un-)registration error', e)
}

if (success) {
callback()
}
}
export const useNotificationRegistrations = (): {
registerNotifications: (safesToRegister: NotifiableSafes, withSignature?: boolean) => Promise<void>
unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise<void>
Expand All @@ -19,43 +36,34 @@ export const useNotificationRegistrations = (): {

const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences()

const registerNotifications = async (safesToRegister: NotifiableSafes, withConfirmationRequests = false) => {
if (!uuid) {
const registerNotifications = async (safesToRegister: NotifiableSafes) => {
if (!uuid || !web3) {
return
}

let didRegister = false

try {
const register = async () => {
const payload = await getRegisterDevicePayload({
uuid,
safesToRegister,
web3: withConfirmationRequests ? web3 : undefined,
web3,
})

// Gateway will return 200 with an empty payload if the device was registered successfully
// @see https://github.com/safe-global/safe-client-gateway-nest/blob/27b6b3846b4ecbf938cdf5d0595ca464c10e556b/src/routes/notifications/notifications.service.ts#L29
const response = await registerDevice(payload)

didRegister = response == null
} catch (e) {
console.error(`Error registering Safe(s)`, e)
return registerDevice(payload)
}

if (!didRegister) {
return
}
await registrationFlow(register(), () => {
_createPreferences(safesToRegister)

_createPreferences(safesToRegister, withConfirmationRequests)

const totalRegistered = Object.values(safesToRegister).reduce((acc, safeAddresses) => acc + safeAddresses.length, 0)
const totalRegistered = Object.values(safesToRegister).reduce(
(acc, safeAddresses) => acc + safeAddresses.length,
0,
)

trackEvent({
...PUSH_NOTIFICATION_EVENTS.REGISTER_SAFES,
label: totalRegistered,
})
trackEvent({
...PUSH_NOTIFICATION_EVENTS.REGISTER_SAFES,
label: totalRegistered,
})

if (!withConfirmationRequests) {
dispatch(
showNotification({
message: `You will now receive notifications for ${
Expand All @@ -65,30 +73,12 @@ export const useNotificationRegistrations = (): {
groupKey: 'notifications',
}),
)
}
}

const unregisterNotifications = async (unregistrationFn: Promise<void>, callback: () => void) => {
let didUnregister = false

try {
const response = await unregistrationFn

didUnregister = response == null
} catch (e) {
console.error('Error unregistering', e)
}

if (!didUnregister) {
return
}

callback()
})
}

const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => {
if (uuid) {
await unregisterNotifications(unregisterSafe(chainId, safeAddress, uuid), () => {
await registrationFlow(unregisterSafe(chainId, safeAddress, uuid), () => {
_deletePreferences({ [chainId]: [safeAddress] })
trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE)
})
Expand All @@ -97,7 +87,7 @@ export const useNotificationRegistrations = (): {

const unregisterChainNotifications = async (chainId: string) => {
if (uuid) {
await unregisterNotifications(unregisterDevice(chainId, uuid), () => {
await registrationFlow(unregisterDevice(chainId, uuid), () => {
_clearPreferences()
trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE)
})
Expand Down
Loading

0 comments on commit 6508339

Please sign in to comment.