From 92106a828084a1b25763fb6e3e4aa058e491265b Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 8 Aug 2023 15:51:14 +0200 Subject: [PATCH 01/62] feat: implement Firebase Cloud Messaging --- .env.example | 11 + .github/workflows/build/action.yml | 9 + package.json | 1 + public/firebase-messaging-sw.js | 17 + .../settings/Notifications/index.tsx | 205 +++++++ .../sidebar/SidebarNavigation/config.tsx | 8 + src/config/constants.ts | 11 + src/config/routes.ts | 3 +- src/hooks/useFirebaseNotifications.ts | 60 ++ src/pages/_app.tsx | 2 + src/pages/settings/notifications.tsx | 23 + src/services/firebase.ts | 31 + yarn.lock | 558 +++++++++++++++++- 13 files changed, 936 insertions(+), 3 deletions(-) create mode 100644 public/firebase-messaging-sw.js create mode 100644 src/components/settings/Notifications/index.tsx create mode 100644 src/hooks/useFirebaseNotifications.ts create mode 100644 src/pages/settings/notifications.tsx create mode 100644 src/services/firebase.ts diff --git a/.env.example b/.env.example index b2804bb7d4..400fd2a56e 100644 --- a/.env.example +++ b/.env.example @@ -32,5 +32,16 @@ NEXT_PUBLIC_CYPRESS_MNEMONIC= NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION= NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING= +# Firebase Cloud Messaging +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= +NEXT_PUBLIC_FIREBASE_DATABASE_URL= +NEXT_PUBLIC_FIREBASE_PROJECT_ID= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= +NEXT_PUBLIC_FIREBASE_VAPID_KEY= + # Redefine NEXT_PUBLIC_REDEFINE_API= \ No newline at end of file diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index df3b0c68e7..19c1b372ac 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,3 +43,12 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} + NEXT_PUBLIC_FIREBASE_API_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_DATABASE_URL: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} + NEXT_PUBLIC_FIREBASE_APP_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} + NEXT_PUBLIC_FIREBASE_VAPID_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY }} diff --git a/package.json b/package.json index b8ad5315b5..8d5794a0a4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "ethereum-blockies-base64": "^1.0.2", "ethers": "5.7.2", "exponential-backoff": "^3.1.0", + "firebase": "^10.1.0", "framer-motion": "^10.13.1", "fuse.js": "^6.6.2", "js-cookie": "^3.0.1", diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 0000000000..07c520c3fe --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,17 @@ +importScripts('https://www.gstatic.com/firebasejs/10.1.0/firebase-app-compat.js') +importScripts('https://www.gstatic.com/firebasejs/10.1.0/firebase-messaging-compat.js') + +const firebaseConfig = Object.fromEntries(new URL(location).searchParams.entries()) + +const app = firebase.initializeApp(firebaseConfig) + +const messaging = firebase.messaging(app) + +messaging.onBackgroundMessage((payload) => { + const { title, body, image } = payload.notification + + self.registration.showNotification(title, { + body, + icon: image, + }) +}) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx new file mode 100644 index 0000000000..849489e8d7 --- /dev/null +++ b/src/components/settings/Notifications/index.tsx @@ -0,0 +1,205 @@ +import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' +import { Grid, Paper, Typography, Button } from '@mui/material' +import { getToken, getMessaging } from 'firebase/messaging' +import type { ReactElement } from 'react' + +import packageJson from '../../../../package.json' +import { FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' +import { useWeb3 } from '@/hooks/wallets/web3' +import useSafeInfo from '@/hooks/useSafeInfo' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { useCurrentChain } from '@/hooks/useChains' +import EthHashInfo from '@/components/common/EthHashInfo' +import { getFirebaseSwRegistrationPath } from '@/services/firebase' + +const NOTIFICATIONS_LS_REGISTRATION_KEY = 'firebaseCloudMessaging' + +const enum DeviceType { + WEB = 'WEB', +} + +type RegisterDeviceDto = { + uuid: string + cloudMessagingToken: string + buildNumber: string + bundle: string + deviceType: DeviceType + version: string + timestamp: string + safeRegistrations: Array<{ + chainId: string + safes: Array + signatures: Array + }> +} + +const getFirebaseToken = async () => { + const firebaseSwPath = getFirebaseSwRegistrationPath() + const swRegistration = await navigator.serviceWorker.getRegistration(firebaseSwPath) + + // Get token + const messaging = getMessaging() + const token = await getToken(messaging, { + vapidKey: FIREBASE_VAPID_KEY, + serviceWorkerRegistration: swRegistration, + }) + + console.log({ token }) + + return token +} + +export const Notifications = (): ReactElement => { + const web3 = useWeb3() + const { safe } = useSafeInfo() + const chain = useCurrentChain() + const [firebase, setFirebase] = useLocalStorage(NOTIFICATIONS_LS_REGISTRATION_KEY) + + const handleRegister = async () => { + const DEVICE_REGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/register/notifications` + + const MESSAGE_PREFIX = 'gnosis-safe' + + if (!web3) { + return + } + + // Request permission to show notifications if not already granted + if (Notification.permission !== 'granted') { + let permission: NotificationPermission | undefined + + try { + permission = await Notification.requestPermission() + } catch (e) { + console.error('Error requesting notification permission', e) + } + + if (permission !== 'granted') { + alert('You must allow notifications to register your device.') + return + } + } + + let didRegister = false + + // Create message to sign + const timestamp = Math.floor(new Date().getTime() / 1000).toString() + const uuid = firebase?.uuid ?? self.crypto.randomUUID() + const safes = [safe.address.value] + const token = await getFirebaseToken() + + const message = MESSAGE_PREFIX + timestamp + uuid + token + safes.join('') + const hashedMessage = keccak256(toUtf8Bytes(message)) + + let payload: RegisterDeviceDto | undefined + + // Register device + try { + const signature = await web3.getSigner().signMessage(hashedMessage) + + // TODO: Push to existing registrations? + payload = { + uuid, + cloudMessagingToken: token, + buildNumber: '0', // TODO: What do we add here? + bundle: '.', // TODO: What do we add here? + deviceType: DeviceType.WEB, + version: packageJson.version, + timestamp, + safeRegistrations: [ + { + chainId: safe.chainId, + safes, + signatures: [signature], + }, + ], + } + + const response = await fetch(DEVICE_REGISTRATION_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + // Gateway will return 200 if the device was registered successfully + didRegister = response?.ok && response?.status === 200 + } catch (e) { + console.error('Error registering device', e) + } + + if (!didRegister) { + alert('Unable to register device.') + } else { + setFirebase(payload) + } + } + + // TODO: Fix unregister + const handleUnregister = async () => { + if (!firebase) { + return + } + + // TODO: Implement on CGW as this is not yet implemented there + const DEVICE_DELETE_ENDPOINT = `${chain?.transactionService}v1/notifications/devices/${firebase.uuid}` + + let didDelete = false + + try { + const response = await fetch(DEVICE_DELETE_ENDPOINT, { + method: 'DELETE', + }) + + console.log({ response: await response.json() }) + + didDelete = response?.ok && response?.status === 200 + } catch (e) { + console.error('Error registering device', e) + } + + if (didDelete) { + setFirebase(undefined) + } + } + + const registeredSafes = firebase?.safeRegistrations.map((safeRegistration) => safeRegistration.safes).flat() + + return ( + + + + + Push notifications + + + + + + {firebase + ? 'You are currently registered to receive notifications about your Safe(s) on this device.' + : 'You can register to see notifications about your Safe(s) on this device. You will have to sign a message to verify that you are the owner of this Safe. Please note that you will need to register again if you clear your browser cache.'} + + + {registeredSafes && ( +
+ {registeredSafes.map((safe, i) => { + return + })} +
+ )} + {firebase ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index dc7df0699c..d3196aa5d6 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -84,6 +84,10 @@ export const settingsNavItems = [ label: 'Appearance', href: AppRoutes.settings.appearance, }, + { + label: 'Notifications', + href: AppRoutes.settings.notifications, + }, { label: 'Modules', href: AppRoutes.settings.modules, @@ -115,6 +119,10 @@ export const generalSettingsNavItems = [ label: 'Appearance', href: AppRoutes.settings.appearance, }, + { + label: 'Notifications', + href: AppRoutes.settings.notifications, + }, { label: 'Data', href: AppRoutes.settings.data, diff --git a/src/config/constants.ts b/src/config/constants.ts index a148aa3caa..67163ff844 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -46,6 +46,17 @@ export const GOOGLE_TAG_MANAGER_AUTH_LIVE = process.env.NEXT_PUBLIC_GOOGLE_TAG_M export const GOOGLE_TAG_MANAGER_AUTH_LATEST = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH || '' export const GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH || '' +// Firebase Cloud Messaging +export const FIREBASE_API_KEY = process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '' +export const FIREBASE_AUTH_DOMAIN = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || '' +export const FIREBASE_DATABASE_URL = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL || '' +export const FIREBASE_PROJECT_ID = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || '' +export const FIREBASE_STORAGE_BUCKET = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || '' +export const FIREBASE_MESSAGING_SENDER_ID = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || '' +export const FIREBASE_APP_ID = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || '' +export const FIREBASE_MEASUREMENT_ID = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID || '' +export const FIREBASE_VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY || '' + // Tenderly - API docs: https://www.notion.so/Simulate-API-Documentation-6f7009fe6d1a48c999ffeb7941efc104 export const TENDERLY_SIMULATE_ENDPOINT_URL = process.env.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL || '' export const TENDERLY_PROJECT_NAME = process.env.NEXT_PUBLIC_TENDERLY_PROJECT_NAME || '' diff --git a/src/config/routes.ts b/src/config/routes.ts index eaa97cbe10..86e5c88b75 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,5 +1,6 @@ export const AppRoutes = { '404': '/404', + _offline: '/_offline', welcome: '/welcome', terms: '/terms', privacy: '/privacy', @@ -9,7 +10,6 @@ export const AppRoutes = { home: '/home', cookie: '/cookie', addressBook: '/address-book', - _offline: '/_offline', apps: { open: '/apps/open', index: '/apps', @@ -27,6 +27,7 @@ export const AppRoutes = { settings: { spendingLimits: '/settings/spending-limits', setup: '/settings/setup', + notifications: '/settings/notifications', modules: '/settings/modules', index: '/settings', environmentVariables: '/settings/environment-variables', diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts new file mode 100644 index 0000000000..418357e8c9 --- /dev/null +++ b/src/hooks/useFirebaseNotifications.ts @@ -0,0 +1,60 @@ +import { useEffect } from 'react' +import { initializeApp } from 'firebase/app' +import { getMessaging, onMessage } from 'firebase/messaging' + +import { useAppDispatch } from '@/store' +import { showNotification } from '@/store/notificationsSlice' +import { FIREBASE_CONFIG, getFirebaseSwRegistrationPath } from '@/services/firebase' + +export const useFirebaseNotifications = (): null => { + const dispatch = useAppDispatch() + + // Register servicer worker + useEffect(() => { + if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { + return + } + + const registerFirebaseSw = () => { + // Firebase normally registers a service worker when calling `getToken` + // but we register it manually to pass custom config from the env + const serviceWorkerPath = getFirebaseSwRegistrationPath() + + navigator.serviceWorker.register(serviceWorkerPath).catch(() => null) + } + + window.addEventListener('load', registerFirebaseSw) + + return () => { + window.removeEventListener('load', registerFirebaseSw) + } + }, []) + + // Listen for messages + useEffect(() => { + if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { + return + } + + // TODO: Should this be added to the privacy policy? + const _app = initializeApp(FIREBASE_CONFIG) + const messaging = getMessaging(_app) + + const unsubscribe = onMessage(messaging, (payload) => { + dispatch( + showNotification({ + message: payload.notification?.title || '', + detailedMessage: payload.notification?.body, + groupKey: payload.messageId, + variant: 'info', + }), + ) + }) + + return () => { + unsubscribe() + } + }, [dispatch]) + + return null +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d6612c4179..7a5e0110bc 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,6 +37,7 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' +import { useFirebaseNotifications } from '@/hooks/useFirebaseNotifications' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -49,6 +50,7 @@ const InitApp = (): null => { useInitOnboard() useInitWeb3() useInitSafeCoreSDK() + useFirebaseNotifications() useTxNotifications() useSafeMessageNotifications() useSafeNotifications() diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx new file mode 100644 index 0000000000..0773776402 --- /dev/null +++ b/src/pages/settings/notifications.tsx @@ -0,0 +1,23 @@ +import Head from 'next/head' +import type { NextPage } from 'next' + +import SettingsHeader from '@/components/settings/SettingsHeader' +import { Notifications } from '@/components/settings/Notifications' + +const NotificationsPage: NextPage = () => { + return ( + <> + + {'Safe{Wallet} – Settings – Notifications'} + + + + +
+ +
+ + ) +} + +export default NotificationsPage diff --git a/src/services/firebase.ts b/src/services/firebase.ts new file mode 100644 index 0000000000..9863142a98 --- /dev/null +++ b/src/services/firebase.ts @@ -0,0 +1,31 @@ +import { + FIREBASE_API_KEY, + FIREBASE_AUTH_DOMAIN, + FIREBASE_DATABASE_URL, + FIREBASE_PROJECT_ID, + FIREBASE_STORAGE_BUCKET, + FIREBASE_MESSAGING_SENDER_ID, + FIREBASE_APP_ID, + FIREBASE_MEASUREMENT_ID, +} from '@/config/constants' + +// TODO: Add these and VAPID_KEY to README.md +export const FIREBASE_CONFIG = { + apiKey: FIREBASE_API_KEY, + authDomain: FIREBASE_AUTH_DOMAIN, + databaseURL: FIREBASE_DATABASE_URL, + projectId: FIREBASE_PROJECT_ID, + storageBucket: FIREBASE_STORAGE_BUCKET, + messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, + appId: FIREBASE_APP_ID, + measurementId: FIREBASE_MEASUREMENT_ID, +} + +export const FIREBASE_SW_PATH = '/firebase-messaging-sw.js' + +export const getFirebaseSwRegistrationPath = (): string => { + const config = new URLSearchParams(FIREBASE_CONFIG).toString() + + // Service workers don't conflict as they are registered with params + return `${FIREBASE_SW_PATH}?${config}` +} diff --git a/yarn.lock b/yarn.lock index 970cab1ae0..b1e0d125ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2127,6 +2127,379 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@firebase/analytics-compat@0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz#50063978c42f13eb800e037e96ac4b17236841f4" + integrity sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q== + dependencies: + "@firebase/analytics" "0.10.0" + "@firebase/analytics-types" "0.8.0" + "@firebase/component" "0.6.4" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.0.tgz#551e744a29adbc07f557306530a2ec86add6d410" + integrity sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw== + +"@firebase/analytics@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.0.tgz#9c6986acd573c6c6189ffb52d0fd63c775db26d7" + integrity sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/installations" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz#e150f61d653a0f2043a34dcb995616a717161839" + integrity sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw== + dependencies: + "@firebase/app-check" "0.8.0" + "@firebase/app-check-types" "0.5.0" + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz#b27ea1397cb80427f729e4bbf3a562f2052955c4" + integrity sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg== + +"@firebase/app-check-types@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.0.tgz#1b02826213d7ce6a1cf773c329b46ea1c67064f4" + integrity sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ== + +"@firebase/app-check@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.0.tgz#b531ec40900af9c3cf1ec63de9094a0ddd733d6a" + integrity sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.15": + version "0.2.15" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.15.tgz#06a932311d340dd94666b9e9cb15ca5fc8bdc434" + integrity sha512-ttEbOEtO1SSz27cRPrwXAmrqDjdQ33sQc7rqqQuSMUuPRdYCQEcYdqzpkbvqgdkzGksx2kfH4JqQ6R/hI12nDw== + dependencies: + "@firebase/app" "0.9.15" + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/app-types@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e" + integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== + +"@firebase/app@0.9.15": + version "0.9.15" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.15.tgz#8c5b7a85c6f856f3292c1fcc2a029c12a63b9ad9" + integrity sha512-xxQi6mkhRjtXeFUwleSF4zU7lwEH+beNhLE7VmkzEzjEsjAS14QPQPZ35gpgSD+/NigOeho7wgEXd4C/bOkRfA== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.4.4.tgz#062dd397a508c7a442f36c014133ded4d29c62bb" + integrity sha512-B2DctJDJ05djBwebNEdC3zbKWzKdIdxpbca8u9P/NSjqaJNSFq3fhz8h8bjlS9ufSrxaQWFSJMMH3dRmx3FlEA== + dependencies: + "@firebase/auth" "1.1.0" + "@firebase/auth-types" "0.12.0" + "@firebase/component" "0.6.4" + "@firebase/util" "1.9.3" + node-fetch "2.6.7" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz#78884f24fa539e34a06c03612c75f222fcc33742" + integrity sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg== + +"@firebase/auth-types@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.0.tgz#f28e1b68ac3b208ad02a15854c585be6da3e8e79" + integrity sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA== + +"@firebase/auth@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.1.0.tgz#106cad08f977245e641642ac9b7c3a2dae46400d" + integrity sha512-5RJQMXG0p/tSvtqpfM8jA+heELjVCgHHASq3F7NglAa/CWUGCAE4g2F4YDPW5stDkvtKKRez0WYAWnbcuQ5P4w== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + "@react-native-async-storage/async-storage" "^1.18.1" + node-fetch "2.6.7" + tslib "^2.1.0" + +"@firebase/component@0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.4.tgz#8981a6818bd730a7554aa5e0516ffc9b1ae3f33d" + integrity sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA== + dependencies: + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/database-compat@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.1.tgz#ab0acbbfb0031080cc16504cef6d00c95cf27ff1" + integrity sha512-ky82yLIboLxtAIWyW/52a6HLMVTzD2kpZlEilVDok73pNPLjkJYowj8iaIWK5nTy7+6Gxt7d00zfjL6zckGdXQ== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/database" "1.0.1" + "@firebase/database-types" "1.0.0" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/database-types@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.0.tgz#3f7f71c2c3fd1e29d15fce513f14dae2e7543f2a" + integrity sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg== + dependencies: + "@firebase/app-types" "0.9.0" + "@firebase/util" "1.9.3" + +"@firebase/database@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.1.tgz#28830f1d0c05ec2f7014658a3165129cec891bcb" + integrity sha512-VAhF7gYwunW4Lw/+RQZvW8dlsf2r0YYqV9W0Gi2Mz8+0TGg1mBJWoUtsHfOr8kPJXhcLsC4eP/z3x6L/Fvjk/A== + dependencies: + "@firebase/auth-interop-types" "0.2.1" + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.14": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.14.tgz#f1ceac5a85da52c6b5b9d65136456e3eaec7b227" + integrity sha512-sOjaYefSPXJXdFH6qyxSwJVakEqAAote6jjrJk/ZCoiX90rs9r3yYV90wP4gmaTKyXjkt8EMlwuapekgGsE5Tw== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/firestore" "4.1.0" + "@firebase/firestore-types" "3.0.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.0.tgz#f3440d5a1cc2a722d361b24cefb62ca8b3577af3" + integrity sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw== + +"@firebase/firestore@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.1.0.tgz#fcdd4e033c258fccbe4d47dadf625faa1f62272f" + integrity sha512-FEd+4R0QL9RAJVcdqXgbdIuQYpvzkeKNBVxNM5qcWDPMurjNpja8VaWpVZmT3JXG8FfO+NGTnHJtsW/nWO7XiQ== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + "@firebase/webchannel-wrapper" "0.10.1" + "@grpc/grpc-js" "~1.8.17" + "@grpc/proto-loader" "^0.6.13" + node-fetch "2.6.7" + tslib "^2.1.0" + +"@firebase/functions-compat@0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.5.tgz#7a532d3a9764c6d5fbc1ec5541a989a704326647" + integrity sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/functions" "0.10.0" + "@firebase/functions-types" "0.6.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.0.tgz#ccd7000dc6fc668f5acb4e6a6a042a877a555ef2" + integrity sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw== + +"@firebase/functions@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.10.0.tgz#c630ddf12cdf941c25bc8d554e30c3226cd560f6" + integrity sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA== + dependencies: + "@firebase/app-check-interop-types" "0.3.0" + "@firebase/auth-interop-types" "0.2.1" + "@firebase/component" "0.6.4" + "@firebase/messaging-interop-types" "0.2.0" + "@firebase/util" "1.9.3" + node-fetch "2.6.7" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.4.tgz#b5557c897b4cd3635a59887a8bf69c3731aaa952" + integrity sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/installations" "0.6.4" + "@firebase/installations-types" "0.5.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.0.tgz#2adad64755cd33648519b573ec7ec30f21fb5354" + integrity sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg== + +"@firebase/installations@0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.4.tgz#20382e33e6062ac5eff4bede8e468ed4c367609e" + integrity sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/util" "1.9.3" + idb "7.0.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.0.tgz#15ecc03c452525f9d47318ad9491b81d1810f113" + integrity sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz#323ca48deef77065b4fcda3cfd662c4337dffcfd" + integrity sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/messaging" "0.12.4" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz#6056f8904a696bf0f7fdcf5f2ca8f008e8f6b064" + integrity sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ== + +"@firebase/messaging@0.12.4": + version "0.12.4" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.4.tgz#ccb49df5ab97d5650c9cf5b8c77ddc34daafcfe0" + integrity sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/installations" "0.6.4" + "@firebase/messaging-interop-types" "0.2.0" + "@firebase/util" "1.9.3" + idb "7.0.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.4.tgz#95cbf32057b5d9f0c75d804bc50e6ed3ba486274" + integrity sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/performance" "0.6.4" + "@firebase/performance-types" "0.2.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.0.tgz#400685f7a3455970817136d9b48ce07a4b9562ff" + integrity sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA== + +"@firebase/performance@0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.4.tgz#0ad766bfcfab4f386f4fe0bef43bbcf505015069" + integrity sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/installations" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/remote-config-compat@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz#1f494c81a6c9560b1f9ca1b4fbd4bbbe47cf4776" + integrity sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/remote-config" "0.4.4" + "@firebase/remote-config-types" "0.3.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz#689900dcdb3e5c059e8499b29db393e4e51314b4" + integrity sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA== + +"@firebase/remote-config@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.4.tgz#6a496117054de58744bc9f382d2a6d1e14060c65" + integrity sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/installations" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.2.tgz#51a97170fd652a516f729f82b97af369e5a2f8d7" + integrity sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/storage" "0.11.2" + "@firebase/storage-types" "0.8.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.0.tgz#f1e40a5361d59240b6e84fac7fbbbb622bfaf707" + integrity sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg== + +"@firebase/storage@0.11.2": + version "0.11.2" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.11.2.tgz#c5e0316543fe1c4026b8e3910f85ad73f5b77571" + integrity sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/util" "1.9.3" + node-fetch "2.6.7" + tslib "^2.1.0" + +"@firebase/util@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.3.tgz#45458dd5cd02d90e55c656e84adf6f3decf4b7ed" + integrity sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA== + dependencies: + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz#60bb2aaf129f9e00621f8d698722ddba6ee1f8ac" + integrity sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw== + "@formatjs/ecma402-abstract@1.11.4": version "1.11.4" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz#b962dfc4ae84361f9f08fbce411b4e4340930eda" @@ -2166,6 +2539,36 @@ dependencies: tslib "^2.1.0" +"@grpc/grpc-js@~1.8.17": + version "1.8.21" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.21.tgz#d282b122c71227859bf6c5866f4c40f4a2696513" + integrity sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.6.13": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" + integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^6.11.3" + yargs "^16.2.0" + +"@grpc/proto-loader@^0.7.0": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.8.tgz#c050bbeae5f000a1919507f195a1b094e218036e" + integrity sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -3134,6 +3537,13 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@react-native-async-storage/async-storage@^1.18.1": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.19.1.tgz#09d35caaa31823b40fdfeebf95decf8f992a6274" + integrity sha512-5QXuGCtB+HL3VtKL2JN3+6t4qh8VXizK+aGDAv6Dqiq3MLrzgZHb4tjVgtEWMd8CcDtD/JqaAI1b6/EaYGtFIA== + dependencies: + merge-options "^3.0.4" + "@reduxjs/toolkit@^1.9.5": version "1.9.5" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" @@ -4150,6 +4560,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -4165,6 +4580,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== +"@types/node@>=12.12.47": + version "20.4.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.6.tgz#b66b66c9bb5d49b199f03399e341c9d6036e9e88" + integrity sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA== + "@types/node@>=13.7.0": version "20.2.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.3.tgz#b31eb300610c3835ac008d690de6f87e28f9b878" @@ -6246,6 +6666,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -8318,6 +8747,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -8434,6 +8870,38 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +firebase@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.1.0.tgz#07281ac2fe4bcf3886eeddcea8903ad17f1aec67" + integrity sha512-ghcdCe2G9DeGmLOrBgR7XPswuc9BFUfjnU93ABopIisMfbJFzoqpSp4emwNiZt+vVGZV1ifeU3DLfhxlujxhCg== + dependencies: + "@firebase/analytics" "0.10.0" + "@firebase/analytics-compat" "0.2.6" + "@firebase/app" "0.9.15" + "@firebase/app-check" "0.8.0" + "@firebase/app-check-compat" "0.3.7" + "@firebase/app-compat" "0.2.15" + "@firebase/app-types" "0.9.0" + "@firebase/auth" "1.1.0" + "@firebase/auth-compat" "0.4.4" + "@firebase/database" "1.0.1" + "@firebase/database-compat" "1.0.1" + "@firebase/firestore" "4.1.0" + "@firebase/firestore-compat" "0.3.14" + "@firebase/functions" "0.10.0" + "@firebase/functions-compat" "0.3.5" + "@firebase/installations" "0.6.4" + "@firebase/installations-compat" "0.2.4" + "@firebase/messaging" "0.12.4" + "@firebase/messaging-compat" "0.2.4" + "@firebase/performance" "0.6.4" + "@firebase/performance-compat" "0.2.4" + "@firebase/remote-config" "0.4.4" + "@firebase/remote-config-compat" "0.2.4" + "@firebase/storage" "0.11.2" + "@firebase/storage-compat" "0.3.2" + "@firebase/util" "1.9.3" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -8963,6 +9431,11 @@ http-https@^1.0.0: resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" integrity sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg== +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -9050,7 +9523,12 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -idb@^7.0.1: +idb@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" + integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== + +idb@7.1.1, idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== @@ -9364,6 +9842,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -10698,6 +11181,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -11747,6 +12237,25 @@ protobufjs@6.11.3, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" +protobufjs@^6.11.3: + version "6.11.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" + integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -12543,7 +13052,7 @@ sade@^1.8.1: dependencies: mri "^1.1.0" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -14373,6 +14882,20 @@ webrtc-adapter@^7.2.1: rtcpeerconnection-shim "^1.2.15" sdp "^2.12.0" +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + websocket@^1.0.32: version "1.0.34" resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" @@ -14867,6 +15390,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -14905,6 +15433,19 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.3.1: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" @@ -14918,6 +15459,19 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 2654d762b631224a1eeb309ba7a17ce848a3038c Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 8 Aug 2023 18:41:50 +0200 Subject: [PATCH 02/62] fix: move to custom `next-pwa` worker --- .gitignore | 1 + next.config.mjs | 5 ++- .../settings/Notifications/index.tsx | 6 +-- src/hooks/useFirebaseNotifications.ts | 44 +++++++++---------- src/services/firebase.ts | 31 ------------- tsconfig.json | 2 +- worker/index.ts | 29 ++++++++++++ 7 files changed, 58 insertions(+), 60 deletions(-) delete mode 100644 src/services/firebase.ts create mode 100644 worker/index.ts diff --git a/.gitignore b/.gitignore index 85fdc04a8a..21b916e6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ yalc.lock /public/sw.js.map /public/workbox-*.js /public/workbox-*.js.map +/public/worker-*.js /public/fallback* \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 722e9f3c76..322dd8e4c7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,9 +1,10 @@ import path from 'path' import withBundleAnalyzer from '@next/bundle-analyzer' -import NextPwa from'next-pwa' +import NextPwa from 'next-pwa' +// If we disable this, it will also disable the Firebase SW +// @see https://github.com/safe-global/safe-wallet-web/pull/2369/commits/92106a828084a1b25763fb6e3e4aa058e491265b const withPWA = NextPwa({ - disable: process.env.NODE_ENV === 'development', dest: 'public', reloadOnOnline: false, /* Do not precache anything */ diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 849489e8d7..28815fc689 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -10,7 +10,6 @@ import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useCurrentChain } from '@/hooks/useChains' import EthHashInfo from '@/components/common/EthHashInfo' -import { getFirebaseSwRegistrationPath } from '@/services/firebase' const NOTIFICATIONS_LS_REGISTRATION_KEY = 'firebaseCloudMessaging' @@ -34,8 +33,9 @@ type RegisterDeviceDto = { } const getFirebaseToken = async () => { - const firebaseSwPath = getFirebaseSwRegistrationPath() - const swRegistration = await navigator.serviceWorker.getRegistration(firebaseSwPath) + const NEXT_PWA_SW_PATH = '/sw.js' + + const swRegistration = await navigator.serviceWorker.getRegistration(NEXT_PWA_SW_PATH) // Get token const messaging = getMessaging() diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index 418357e8c9..7600b09c48 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -4,32 +4,20 @@ import { getMessaging, onMessage } from 'firebase/messaging' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' -import { FIREBASE_CONFIG, getFirebaseSwRegistrationPath } from '@/services/firebase' +import { + FIREBASE_API_KEY, + FIREBASE_AUTH_DOMAIN, + FIREBASE_DATABASE_URL, + FIREBASE_PROJECT_ID, + FIREBASE_STORAGE_BUCKET, + FIREBASE_MESSAGING_SENDER_ID, + FIREBASE_APP_ID, + FIREBASE_MEASUREMENT_ID, +} from '@/config/constants' export const useFirebaseNotifications = (): null => { const dispatch = useAppDispatch() - // Register servicer worker - useEffect(() => { - if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { - return - } - - const registerFirebaseSw = () => { - // Firebase normally registers a service worker when calling `getToken` - // but we register it manually to pass custom config from the env - const serviceWorkerPath = getFirebaseSwRegistrationPath() - - navigator.serviceWorker.register(serviceWorkerPath).catch(() => null) - } - - window.addEventListener('load', registerFirebaseSw) - - return () => { - window.removeEventListener('load', registerFirebaseSw) - } - }, []) - // Listen for messages useEffect(() => { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { @@ -37,7 +25,17 @@ export const useFirebaseNotifications = (): null => { } // TODO: Should this be added to the privacy policy? - const _app = initializeApp(FIREBASE_CONFIG) + const _app = initializeApp({ + apiKey: FIREBASE_API_KEY, + authDomain: FIREBASE_AUTH_DOMAIN, + databaseURL: FIREBASE_DATABASE_URL, + projectId: FIREBASE_PROJECT_ID, + storageBucket: FIREBASE_STORAGE_BUCKET, + messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, + appId: FIREBASE_APP_ID, + measurementId: FIREBASE_MEASUREMENT_ID, + }) + const messaging = getMessaging(_app) const unsubscribe = onMessage(messaging, (payload) => { diff --git a/src/services/firebase.ts b/src/services/firebase.ts deleted file mode 100644 index 9863142a98..0000000000 --- a/src/services/firebase.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - FIREBASE_API_KEY, - FIREBASE_AUTH_DOMAIN, - FIREBASE_DATABASE_URL, - FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET, - FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_APP_ID, - FIREBASE_MEASUREMENT_ID, -} from '@/config/constants' - -// TODO: Add these and VAPID_KEY to README.md -export const FIREBASE_CONFIG = { - apiKey: FIREBASE_API_KEY, - authDomain: FIREBASE_AUTH_DOMAIN, - databaseURL: FIREBASE_DATABASE_URL, - projectId: FIREBASE_PROJECT_ID, - storageBucket: FIREBASE_STORAGE_BUCKET, - messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, - appId: FIREBASE_APP_ID, - measurementId: FIREBASE_MEASUREMENT_ID, -} - -export const FIREBASE_SW_PATH = '/firebase-messaging-sw.js' - -export const getFirebaseSwRegistrationPath = (): string => { - const config = new URLSearchParams(FIREBASE_CONFIG).toString() - - // Service workers don't conflict as they are registered with params - return `${FIREBASE_SW_PATH}?${config}` -} diff --git a/tsconfig.json b/tsconfig.json index 045cbd3169..cb1274960b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2015", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "esnext", "webworker"], "allowJs": true, "skipLibCheck": true, "strict": true, diff --git a/worker/index.ts b/worker/index.ts new file mode 100644 index 0000000000..f61aff3fb1 --- /dev/null +++ b/worker/index.ts @@ -0,0 +1,29 @@ +import { initializeApp } from 'firebase/app' +import { getMessaging } from 'firebase/messaging/sw' +import { onBackgroundMessage } from 'firebase/messaging/sw' + +declare let self: ServiceWorkerGlobalScope + + // To disable all workbox logging during development, you can set self.__WB_DISABLE_DEV_LOGS to true + // https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging +;(self as any).__WB_DISABLE_DEV_LOGS = true + +const app = initializeApp({ + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, +}) + +const messaging = getMessaging(app) + +onBackgroundMessage(messaging, (payload) => { + self.registration.showNotification(payload.notification?.title || '', { + body: payload.notification?.body, + icon: payload.notification?.image, + }) +}) From d3b8624ae912b595295d693720544f312092330c Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 9 Aug 2023 10:12:01 +0200 Subject: [PATCH 03/62] Revert "fix: move to custom `next-pwa` worker" This reverts commit 2654d762b631224a1eeb309ba7a17ce848a3038c. --- .gitignore | 1 - next.config.mjs | 5 +-- .../settings/Notifications/index.tsx | 6 +-- src/hooks/useFirebaseNotifications.ts | 44 ++++++++++--------- src/services/firebase.ts | 31 +++++++++++++ tsconfig.json | 2 +- worker/index.ts | 29 ------------ 7 files changed, 60 insertions(+), 58 deletions(-) create mode 100644 src/services/firebase.ts delete mode 100644 worker/index.ts diff --git a/.gitignore b/.gitignore index 21b916e6d8..85fdc04a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,5 +51,4 @@ yalc.lock /public/sw.js.map /public/workbox-*.js /public/workbox-*.js.map -/public/worker-*.js /public/fallback* \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 322dd8e4c7..722e9f3c76 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,10 +1,9 @@ import path from 'path' import withBundleAnalyzer from '@next/bundle-analyzer' -import NextPwa from 'next-pwa' +import NextPwa from'next-pwa' -// If we disable this, it will also disable the Firebase SW -// @see https://github.com/safe-global/safe-wallet-web/pull/2369/commits/92106a828084a1b25763fb6e3e4aa058e491265b const withPWA = NextPwa({ + disable: process.env.NODE_ENV === 'development', dest: 'public', reloadOnOnline: false, /* Do not precache anything */ diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 28815fc689..849489e8d7 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -10,6 +10,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useCurrentChain } from '@/hooks/useChains' import EthHashInfo from '@/components/common/EthHashInfo' +import { getFirebaseSwRegistrationPath } from '@/services/firebase' const NOTIFICATIONS_LS_REGISTRATION_KEY = 'firebaseCloudMessaging' @@ -33,9 +34,8 @@ type RegisterDeviceDto = { } const getFirebaseToken = async () => { - const NEXT_PWA_SW_PATH = '/sw.js' - - const swRegistration = await navigator.serviceWorker.getRegistration(NEXT_PWA_SW_PATH) + const firebaseSwPath = getFirebaseSwRegistrationPath() + const swRegistration = await navigator.serviceWorker.getRegistration(firebaseSwPath) // Get token const messaging = getMessaging() diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index 7600b09c48..418357e8c9 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -4,20 +4,32 @@ import { getMessaging, onMessage } from 'firebase/messaging' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' -import { - FIREBASE_API_KEY, - FIREBASE_AUTH_DOMAIN, - FIREBASE_DATABASE_URL, - FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET, - FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_APP_ID, - FIREBASE_MEASUREMENT_ID, -} from '@/config/constants' +import { FIREBASE_CONFIG, getFirebaseSwRegistrationPath } from '@/services/firebase' export const useFirebaseNotifications = (): null => { const dispatch = useAppDispatch() + // Register servicer worker + useEffect(() => { + if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { + return + } + + const registerFirebaseSw = () => { + // Firebase normally registers a service worker when calling `getToken` + // but we register it manually to pass custom config from the env + const serviceWorkerPath = getFirebaseSwRegistrationPath() + + navigator.serviceWorker.register(serviceWorkerPath).catch(() => null) + } + + window.addEventListener('load', registerFirebaseSw) + + return () => { + window.removeEventListener('load', registerFirebaseSw) + } + }, []) + // Listen for messages useEffect(() => { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { @@ -25,17 +37,7 @@ export const useFirebaseNotifications = (): null => { } // TODO: Should this be added to the privacy policy? - const _app = initializeApp({ - apiKey: FIREBASE_API_KEY, - authDomain: FIREBASE_AUTH_DOMAIN, - databaseURL: FIREBASE_DATABASE_URL, - projectId: FIREBASE_PROJECT_ID, - storageBucket: FIREBASE_STORAGE_BUCKET, - messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, - appId: FIREBASE_APP_ID, - measurementId: FIREBASE_MEASUREMENT_ID, - }) - + const _app = initializeApp(FIREBASE_CONFIG) const messaging = getMessaging(_app) const unsubscribe = onMessage(messaging, (payload) => { diff --git a/src/services/firebase.ts b/src/services/firebase.ts new file mode 100644 index 0000000000..9863142a98 --- /dev/null +++ b/src/services/firebase.ts @@ -0,0 +1,31 @@ +import { + FIREBASE_API_KEY, + FIREBASE_AUTH_DOMAIN, + FIREBASE_DATABASE_URL, + FIREBASE_PROJECT_ID, + FIREBASE_STORAGE_BUCKET, + FIREBASE_MESSAGING_SENDER_ID, + FIREBASE_APP_ID, + FIREBASE_MEASUREMENT_ID, +} from '@/config/constants' + +// TODO: Add these and VAPID_KEY to README.md +export const FIREBASE_CONFIG = { + apiKey: FIREBASE_API_KEY, + authDomain: FIREBASE_AUTH_DOMAIN, + databaseURL: FIREBASE_DATABASE_URL, + projectId: FIREBASE_PROJECT_ID, + storageBucket: FIREBASE_STORAGE_BUCKET, + messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, + appId: FIREBASE_APP_ID, + measurementId: FIREBASE_MEASUREMENT_ID, +} + +export const FIREBASE_SW_PATH = '/firebase-messaging-sw.js' + +export const getFirebaseSwRegistrationPath = (): string => { + const config = new URLSearchParams(FIREBASE_CONFIG).toString() + + // Service workers don't conflict as they are registered with params + return `${FIREBASE_SW_PATH}?${config}` +} diff --git a/tsconfig.json b/tsconfig.json index cb1274960b..045cbd3169 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2015", - "lib": ["dom", "dom.iterable", "esnext", "webworker"], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, diff --git a/worker/index.ts b/worker/index.ts deleted file mode 100644 index f61aff3fb1..0000000000 --- a/worker/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { initializeApp } from 'firebase/app' -import { getMessaging } from 'firebase/messaging/sw' -import { onBackgroundMessage } from 'firebase/messaging/sw' - -declare let self: ServiceWorkerGlobalScope - - // To disable all workbox logging during development, you can set self.__WB_DISABLE_DEV_LOGS to true - // https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging -;(self as any).__WB_DISABLE_DEV_LOGS = true - -const app = initializeApp({ - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, -}) - -const messaging = getMessaging(app) - -onBackgroundMessage(messaging, (payload) => { - self.registration.showNotification(payload.notification?.title || '', { - body: payload.notification?.body, - icon: payload.notification?.image, - }) -}) From 6fded4a692b1952f928af0b2c1997e4761d4fcfd Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 9 Aug 2023 10:32:43 +0200 Subject: [PATCH 04/62] fix: remove `next-pwa` --- next.config.mjs | 12 +- package.json | 1 - yarn.lock | 757 ++---------------------------------------------- 3 files changed, 26 insertions(+), 744 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 722e9f3c76..ef70c1f665 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,15 +1,5 @@ import path from 'path' import withBundleAnalyzer from '@next/bundle-analyzer' -import NextPwa from'next-pwa' - -const withPWA = NextPwa({ - disable: process.env.NODE_ENV === 'development', - dest: 'public', - reloadOnOnline: false, - /* Do not precache anything */ - publicExcludes: ['**/*'], - buildExcludes: [/./], -}) /** @type {import('next').NextConfig} */ const nextConfig = { @@ -83,4 +73,4 @@ const nextConfig = { export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', -})(withPWA(nextConfig)) +})(nextConfig) diff --git a/package.json b/package.json index 8d5794a0a4..f3042e736e 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "js-cookie": "^3.0.1", "lodash": "^4.17.21", "next": "12.2.0", - "next-pwa": "^5.6.0", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", "react": "18.2.0", diff --git a/yarn.lock b/yarn.lock index b1e0d125ed..8cfbbd9e09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,15 +15,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@apideck/better-ajv-errors@^0.3.1": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" - integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== - dependencies: - json-schema "^0.4.0" - jsonpointer "^5.0.0" - leven "^3.1.0" - "@apocentre/alias-sampling@^0.5.3": version "0.5.3" resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" @@ -41,7 +32,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== -"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.19.6": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.19.6": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== @@ -163,7 +154,7 @@ dependencies: "@babel/types" "^7.18.9" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== @@ -875,7 +866,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.19.4": +"@babel/preset-env@^7.19.4": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== @@ -996,7 +987,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.11" -"@babel/runtime@7.20.6", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@7.20.6", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.20.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== @@ -2843,7 +2834,7 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": +"@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== @@ -2862,14 +2853,6 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" @@ -2883,7 +2866,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -3554,43 +3537,6 @@ redux-thunk "^2.4.2" reselect "^4.1.8" -"@rollup/plugin-babel@^5.2.0": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" - integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@rollup/pluginutils" "^3.1.0" - -"@rollup/plugin-node-resolve@^11.2.1": - version "11.2.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" - integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.19.0" - -"@rollup/plugin-replace@^2.4.1": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" - integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" - -"@rollup/pluginutils@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" - "@rushstack/eslint-patch@^1.1.3": version "1.2.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" @@ -3987,16 +3933,6 @@ "@stablelib/random" "^1.0.2" "@stablelib/wipe" "^1.0.1" -"@surma/rollup-plugin-off-main-thread@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" - integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== - dependencies: - ejs "^3.1.6" - json5 "^2.2.0" - magic-string "^0.25.0" - string.prototype.matchall "^4.0.6" - "@svgr/babel-plugin-add-jsx-attribute@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" @@ -4444,11 +4380,6 @@ dependencies: "@types/node" "*" -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - "@types/ethereum-protocol@*", "@types/ethereum-protocol@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.2.tgz#e765d4c6f4b5ebe906932bd20333e307c56a9bc7" @@ -4456,14 +4387,6 @@ dependencies: bignumber.js "7.2.1" -"@types/glob@^7.1.1": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" - integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -4533,7 +4456,7 @@ "@types/parse5" "^6.0.3" "@types/tough-cookie" "*" -"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -4565,11 +4488,6 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/minimatch@*": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" - integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== - "@types/node@*": version "18.11.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" @@ -4718,13 +4636,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -5561,12 +5472,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -5576,16 +5482,6 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.6.0: - version "8.11.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" - integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -5698,23 +5594,11 @@ array-includes@^3.1.4, array-includes@^3.1.5, array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== - dependencies: - array-uniq "^1.0.1" - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== - array.prototype.flat@^1.2.5: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" @@ -5819,7 +5703,7 @@ async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0: dependencies: lodash "^4.17.14" -async@^3.2.0, async@^3.2.3: +async@^3.2.0: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -5891,16 +5775,6 @@ babel-jest@^28.1.3: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@^8.2.5: - version "8.3.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" - integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -6047,11 +5921,6 @@ big-integer@^1.6.48: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - bigint-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" @@ -6197,13 +6066,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -6410,11 +6272,6 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" -builtin-modules@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - bytebuffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" @@ -6517,7 +6374,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -6606,13 +6463,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-webpack-plugin@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" - integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== - dependencies: - del "^4.1.1" - cli-color@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.3.tgz#73769ba969080629670f3f2ef69a4bf4e7cc1879" @@ -6777,7 +6627,7 @@ command-line-usage@^6.1.0: table-layout "^1.0.2" typical "^5.2.0" -commander@^2.20.0, commander@^2.20.3: +commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -6802,11 +6652,6 @@ common-tags@^1.8.0: resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -7028,11 +6873,6 @@ crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -7309,19 +7149,6 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -7543,13 +7370,6 @@ eip55@^2.1.0: dependencies: keccak "^3.0.3" -ejs@^3.1.6: - version "3.1.8" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" - integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== - dependencies: - jake "^10.8.5" - electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -7588,11 +7408,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - encode-utf8@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" @@ -8047,11 +7862,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - estree-walker@^2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" @@ -8715,7 +8525,7 @@ fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -8794,13 +8604,6 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -filelist@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -8826,15 +8629,6 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" -find-cache-dir@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - find-replace@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" @@ -8992,7 +8786,7 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1, fs-extra@^9.1.0: +fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -9068,11 +8862,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -9143,7 +8932,7 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -9187,7 +8976,7 @@ globalyzer@0.1.0: resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== -globby@^11.0.4, globby@^11.1.0: +globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -9210,17 +8999,6 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - globrex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" @@ -9528,7 +9306,7 @@ idb@7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== -idb@7.1.1, idb@^7.0.1: +idb@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== @@ -9783,11 +9561,6 @@ is-map@^2.0.1, is-map@^2.0.2: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== - is-nan@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" @@ -9813,30 +9586,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -9865,11 +9614,6 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - is-set@^2.0.1, is-set@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" @@ -10026,16 +9770,6 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" - jayson@^3.4.4: version "3.7.0" resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25" @@ -10460,24 +10194,6 @@ jest-watcher@^28.1.3: jest-util "^28.1.3" string-length "^4.0.1" -jest-worker@^26.2.1: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" @@ -10637,12 +10353,7 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-schema@0.4.0, json-schema@^0.4.0: +json-schema@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== @@ -10671,7 +10382,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: +json5@^2.1.3, json5@^2.2.1, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -10702,11 +10413,6 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonpointer@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" - integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== - jsonschema@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" @@ -10934,15 +10640,6 @@ lit@2.7.5: lit-element "^3.3.0" lit-html "^2.7.0" -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -11000,11 +10697,6 @@ lodash.partition@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.partition/-/lodash.partition-4.6.0.tgz#a38e46b73469e0420b0da1212e66d414be364ba4" integrity sha512-35L3dSF3Q6V1w5j6V3NhNlQjzsRDC/pYKCTdYTmwqSib+Q8ponkAmt/PwEOq3EmI38DSCl+SkIVwLd+uSlVdrg== -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== - lodash.uniqby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" @@ -11097,13 +10789,6 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== -magic-string@^0.25.0, magic-string@^0.25.7: - version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" - integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== - dependencies: - sourcemap-codec "^1.4.8" - make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -11112,7 +10797,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -11294,13 +10979,6 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff" - integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.6, minimist@~1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -11484,18 +11162,6 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -next-pwa@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4" - integrity sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A== - dependencies: - babel-loader "^8.2.5" - clean-webpack-plugin "^4.0.0" - globby "^11.0.4" - terser-webpack-plugin "^5.3.3" - workbox-webpack-plugin "^6.5.4" - workbox-window "^6.5.4" - next-tick@1, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -11606,7 +11272,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -11805,11 +11471,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -11895,11 +11556,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -11946,12 +11602,12 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.0.0, pify@^2.2.0: +pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== @@ -11971,18 +11627,6 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== - pino-abstract-transport@v0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" @@ -12018,7 +11662,7 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -12146,7 +11790,7 @@ prettier@^2.3.1, prettier@^2.7.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== -pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: +pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -12790,11 +12434,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -12890,13 +12529,6 @@ rifm@^0.12.1: resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4" integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -12975,23 +12607,6 @@ rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4: dependencies: bn.js "^5.2.0" -rollup-plugin-terser@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" - integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== - dependencies: - "@babel/code-frame" "^7.10.4" - jest-worker "^26.2.1" - serialize-javascript "^4.0.0" - terser "^5.0.0" - -rollup@^2.43.1: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== - optionalDependencies: - fsevents "~2.3.2" - rpc-websockets@^7.5.0: version "7.5.0" resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748" @@ -13137,24 +12752,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" @@ -13234,20 +12831,6 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - serve-static@1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" @@ -13420,11 +13003,6 @@ sonic-boom@^2.2.1: dependencies: atomic-sleep "^1.0.0" -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -13438,14 +13016,6 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -13461,18 +13031,6 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@^0.8.0-beta.0: - version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" - integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== - dependencies: - whatwg-url "^7.0.0" - -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - spawn-sync@^1.0.15: version "1.0.15" resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" @@ -13582,7 +13140,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== @@ -13633,15 +13191,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -13666,11 +13215,6 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-comments@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" - integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -13872,21 +13416,6 @@ tar@^4.0.2: safe-buffer "^5.2.1" yallist "^3.1.1" -temp-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" - integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== - -tempy@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" - integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== - dependencies: - is-stream "^2.0.0" - temp-dir "^2.0.0" - type-fest "^0.16.0" - unique-string "^2.0.0" - terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -13895,27 +13424,6 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.3.3: - version "5.3.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" - integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== - dependencies: - "@jridgewell/trace-mapping" "^0.3.14" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - terser "^5.14.1" - -terser@^5.0.0, terser@^5.14.1: - version "5.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880" - integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -14057,13 +13565,6 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== - dependencies: - punycode "^2.1.0" - tr46@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" @@ -14222,11 +13723,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" - integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -14379,13 +13875,6 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -14411,11 +13900,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -upath@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - update-browserslist-db@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -14841,11 +14325,6 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -14866,14 +14345,6 @@ webpack-bundle-analyzer@4.7.0: sirv "^1.0.7" ws "^7.3.1" -webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - webrtc-adapter@^7.2.1: version "7.7.1" resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz#b2c227a6144983b35057df67bd984a7d4bfd17f1" @@ -14954,15 +14425,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -15042,175 +14504,6 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" -workbox-background-sync@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9" - integrity sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g== - dependencies: - idb "^7.0.1" - workbox-core "6.5.4" - -workbox-broadcast-update@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz#8441cff5417cd41f384ba7633ca960a7ffe40f66" - integrity sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw== - dependencies: - workbox-core "6.5.4" - -workbox-build@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.5.4.tgz#7d06d31eb28a878817e1c991c05c5b93409f0389" - integrity sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA== - dependencies: - "@apideck/better-ajv-errors" "^0.3.1" - "@babel/core" "^7.11.1" - "@babel/preset-env" "^7.11.0" - "@babel/runtime" "^7.11.2" - "@rollup/plugin-babel" "^5.2.0" - "@rollup/plugin-node-resolve" "^11.2.1" - "@rollup/plugin-replace" "^2.4.1" - "@surma/rollup-plugin-off-main-thread" "^2.2.3" - ajv "^8.6.0" - common-tags "^1.8.0" - fast-json-stable-stringify "^2.1.0" - fs-extra "^9.0.1" - glob "^7.1.6" - lodash "^4.17.20" - pretty-bytes "^5.3.0" - rollup "^2.43.1" - rollup-plugin-terser "^7.0.0" - source-map "^0.8.0-beta.0" - stringify-object "^3.3.0" - strip-comments "^2.0.1" - tempy "^0.6.0" - upath "^1.2.0" - workbox-background-sync "6.5.4" - workbox-broadcast-update "6.5.4" - workbox-cacheable-response "6.5.4" - workbox-core "6.5.4" - workbox-expiration "6.5.4" - workbox-google-analytics "6.5.4" - workbox-navigation-preload "6.5.4" - workbox-precaching "6.5.4" - workbox-range-requests "6.5.4" - workbox-recipes "6.5.4" - workbox-routing "6.5.4" - workbox-strategies "6.5.4" - workbox-streams "6.5.4" - workbox-sw "6.5.4" - workbox-window "6.5.4" - -workbox-cacheable-response@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz#a5c6ec0c6e2b6f037379198d4ef07d098f7cf137" - integrity sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug== - dependencies: - workbox-core "6.5.4" - -workbox-core@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.5.4.tgz#df48bf44cd58bb1d1726c49b883fb1dffa24c9ba" - integrity sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q== - -workbox-expiration@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz#501056f81e87e1d296c76570bb483ce5e29b4539" - integrity sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ== - dependencies: - idb "^7.0.1" - workbox-core "6.5.4" - -workbox-google-analytics@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz#c74327f80dfa4c1954cbba93cd7ea640fe7ece7d" - integrity sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg== - dependencies: - workbox-background-sync "6.5.4" - workbox-core "6.5.4" - workbox-routing "6.5.4" - workbox-strategies "6.5.4" - -workbox-navigation-preload@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz#ede56dd5f6fc9e860a7e45b2c1a8f87c1c793212" - integrity sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng== - dependencies: - workbox-core "6.5.4" - -workbox-precaching@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz#740e3561df92c6726ab5f7471e6aac89582cab72" - integrity sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg== - dependencies: - workbox-core "6.5.4" - workbox-routing "6.5.4" - workbox-strategies "6.5.4" - -workbox-range-requests@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz#86b3d482e090433dab38d36ae031b2bb0bd74399" - integrity sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg== - dependencies: - workbox-core "6.5.4" - -workbox-recipes@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz#cca809ee63b98b158b2702dcfb741b5cc3e24acb" - integrity sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA== - dependencies: - workbox-cacheable-response "6.5.4" - workbox-core "6.5.4" - workbox-expiration "6.5.4" - workbox-precaching "6.5.4" - workbox-routing "6.5.4" - workbox-strategies "6.5.4" - -workbox-routing@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.5.4.tgz#6a7fbbd23f4ac801038d9a0298bc907ee26fe3da" - integrity sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg== - dependencies: - workbox-core "6.5.4" - -workbox-strategies@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz#4edda035b3c010fc7f6152918370699334cd204d" - integrity sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw== - dependencies: - workbox-core "6.5.4" - -workbox-streams@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.5.4.tgz#1cb3c168a6101df7b5269d0353c19e36668d7d69" - integrity sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg== - dependencies: - workbox-core "6.5.4" - workbox-routing "6.5.4" - -workbox-sw@6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.5.4.tgz#d93e9c67924dd153a61367a4656ff4d2ae2ed736" - integrity sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA== - -workbox-webpack-plugin@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz#baf2d3f4b8f435f3469887cf4fba2b7fac3d0fd7" - integrity sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg== - dependencies: - fast-json-stable-stringify "^2.1.0" - pretty-bytes "^5.4.1" - upath "^1.2.0" - webpack-sources "^1.4.3" - workbox-build "6.5.4" - -workbox-window@6.5.4, workbox-window@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.5.4.tgz#d991bc0a94dff3c2dbb6b84558cff155ca878e91" - integrity sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug== - dependencies: - "@types/trusted-types" "^2.0.2" - workbox-core "6.5.4" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" From ae952c5d9ddfaaf6ef020d1175597916f0ef8dd1 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 9 Aug 2023 12:52:54 +0200 Subject: [PATCH 05/62] fix: convert SW to TS + add notification text --- .gitignore | 3 + next.config.mjs | 35 +- package.json | 3 +- public/firebase-messaging-sw.js | 17 - public/firebase-messaging-sw.ts | 246 +++ .../settings/Notifications/index.tsx | 6 +- src/config/constants.ts | 1 + src/hooks/useFirebaseNotifications.ts | 30 +- src/services/firebase.ts | 31 - yarn.lock | 1483 ++++++++++++++++- 10 files changed, 1781 insertions(+), 74 deletions(-) delete mode 100644 public/firebase-messaging-sw.js create mode 100644 public/firebase-messaging-sw.ts delete mode 100644 src/services/firebase.ts diff --git a/.gitignore b/.gitignore index 85fdc04a8a..c15f10734c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,9 @@ yalc.lock /cypress/screenshots /cypress/downloads +/public/firebase-messaging-sw.js + +# TODO: Remove after we have all deleted these locally - next-pwa remnants /public/sw.js /public/sw.js.map /public/workbox-*.js diff --git a/next.config.mjs b/next.config.mjs index ef70c1f665..9d9186efee 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,5 @@ import path from 'path' +import { InjectManifest } from 'workbox-webpack-plugin' import withBundleAnalyzer from '@next/bundle-analyzer' /** @type {import('next').NextConfig} */ @@ -35,7 +36,7 @@ const nextConfig = { }, ] }, - webpack(config) { + webpack(config, context) { config.module.rules.push({ test: /\.svg$/i, issuer: { and: [/\.(js|ts|md)x?$/] }, @@ -61,6 +62,38 @@ const nextConfig = { ], }) + // Enable hot reloading/compilation of the Firebase service worker in Typescript + // @see https://github.com/vercel/next.js/issues/33863#issuecomment-1140518693 + if (!context.isServer) { + const swSrc = path.join(context.dir, 'public', '/firebase-messaging-sw.ts') + const swDest = path.join(context.dir, 'public', '/firebase-messaging-sw.js') + + const workboxPlugin = new InjectManifest({ + swSrc, + swDest, + include: ['__nothing__'], + }) + + if (context.dev) { + // Suppress the "InjectManifest has been called multiple times" warning by reaching into + // the private properties of the plugin and making sure it never ends up in the state + // where it makes that warning. + // https://github.com/GoogleChrome/workbox/blob/v6/packages/workbox-webpack-plugin/src/inject-manifest.ts#L260-L282 + // @see https://github.com/GoogleChrome/workbox/issues/1790#issuecomment-1241356293 + Object.defineProperty(workboxPlugin, 'alreadyCalled', { + get() { + return false + }, + set() { + // do nothing; the internals try to set it to true, which then results in a warning + // on the next run of webpack. + }, + }) + } + + config.plugins.push(workboxPlugin) + } + config.resolve.alias = { ...config.resolve.alias, 'bn.js': path.resolve('./node_modules/bn.js/lib/bn.js'), diff --git a/package.json b/package.json index f3042e736e..04135ca0b4 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "typechain": "^8.0.0", "typescript": "4.9.4", "typescript-plugin-css-modules": "^4.2.2", - "whatwg-fetch": "3.6.2" + "whatwg-fetch": "3.6.2", + "workbox-webpack-plugin": "^7.0.0" } } diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js deleted file mode 100644 index 07c520c3fe..0000000000 --- a/public/firebase-messaging-sw.js +++ /dev/null @@ -1,17 +0,0 @@ -importScripts('https://www.gstatic.com/firebasejs/10.1.0/firebase-app-compat.js') -importScripts('https://www.gstatic.com/firebasejs/10.1.0/firebase-messaging-compat.js') - -const firebaseConfig = Object.fromEntries(new URL(location).searchParams.entries()) - -const app = firebase.initializeApp(firebaseConfig) - -const messaging = firebase.messaging(app) - -messaging.onBackgroundMessage((payload) => { - const { title, body, image } = payload.notification - - self.registration.showNotification(title, { - body, - icon: image, - }) -}) diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts new file mode 100644 index 0000000000..37f1cb7cf5 --- /dev/null +++ b/public/firebase-messaging-sw.ts @@ -0,0 +1,246 @@ +/// + +import { initializeApp } from 'firebase/app' +import { onBackgroundMessage } from 'firebase/messaging/sw' +import { getMessaging } from 'firebase/messaging/sw' +import type { MessagePayload } from 'firebase/messaging/sw' + +// Default type of `self` is `WorkerGlobalScope & typeof globalThis` +// https://github.com/microsoft/TypeScript/issues/14877 +declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown } + +// Satisfy Workbox +self.__WB_MANIFEST + +// TODO: Remove those which aren't used for notifications +// https://github.com/safe-global/safe-transaction-service/blob/c562ef3a43f77f6d38e9ea704e1434394598aa30/safe_transaction_service/history/signals.py#L138 +enum WebhookType { + NEW_CONFIRMATION = 'NEW_CONFIRMATION', + EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', + PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', + INCOMING_ETHER = 'INCOMING_ETHER', + OUTGOING_ETHER = 'OUTGOING_ETHER', + INCOMING_TOKEN = 'INCOMING_TOKEN', + OUTGOING_TOKEN = 'OUTGOING_TOKEN', + SAFE_CREATED = 'SAFE_CREATED', + MODULE_TRANSACTION = 'MODULE_TRANSACTION', + CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook +} + +type NewConfirmationEvent = { + type: WebhookType.NEW_CONFIRMATION + chainId: string + address: string + owner: string + safeTxHash: string +} + +type ExecutedMultisigTransactionEvent = { + type: WebhookType.EXECUTED_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string + failed: boolean + txHash: string +} + +type PendingMultisigTransactionEvent = { + type: WebhookType.PENDING_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string +} + +type IncomingEtherEvent = { + type: WebhookType.INCOMING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +type OutgoingEtherEvent = { + type: WebhookType.OUTGOING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +type IncomingTokenEvent = { + type: WebhookType.INCOMING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +type OutgoingTokenEvent = { + type: WebhookType.OUTGOING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +type SafeCreatedEvent = { + type: WebhookType.SAFE_CREATED + chainId: string + address: string + txHash: string + blockNumber: string +} + +type ModuleTransactionEvent = { + type: WebhookType.MODULE_TRANSACTION + chainId: string + address: string + module: string + txHash: string +} + +type ConfirmationRequest = { + type: WebhookType.CONFIRMATION_REQUEST + chainId: string + address: string + safeTxHash: string +} + +type WebhookEvent = + | NewConfirmationEvent + | ExecutedMultisigTransactionEvent + | PendingMultisigTransactionEvent + | IncomingEtherEvent + | OutgoingEtherEvent + | IncomingTokenEvent + | OutgoingTokenEvent + | SafeCreatedEvent + | ModuleTransactionEvent + | ConfirmationRequest + +const app = initializeApp({ + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, +}) + +const messaging = getMessaging(app) + +const isWebhookEvent = (data: T): data is T & { ['data']: WebhookEvent } => { + return Object.values(WebhookType).some((type) => type === data?.type) +} + +onBackgroundMessage(messaging, (payload) => { + // TODO: Add default values + let title, body, image + + if (payload.notification) { + ;({ title, body, image } = payload.notification) + } + + if (isWebhookEvent(payload.data)) { + switch (payload.data.type) { + case WebhookType.NEW_CONFIRMATION: { + const { address, chainId, owner, safeTxHash } = payload.data + + title = `New confirmation for ${safeTxHash}` + body = `Safe ${address} on chain ${chainId} has a new confirmation from ${owner}.` + + break + } + case WebhookType.EXECUTED_MULTISIG_TRANSACTION: { + const { address, chainId, failed, safeTxHash, txHash } = payload.data + + title = failed ? `Transaction ${safeTxHash} failed` : `Transaction ${safeTxHash} executed` + body = failed + ? `Safe ${address} on chain ${chainId} failed to execute transaction ${txHash}.` + : `Safe ${address} on chain ${chainId} executed transaction ${txHash}.` + + break + } + case WebhookType.PENDING_MULTISIG_TRANSACTION: { + const { address, chainId, safeTxHash } = payload.data + + title = `New pending transaction for ${safeTxHash}` + body = `Safe ${address} on chain ${chainId} has a new pending transaction ${safeTxHash}.` + + break + } + case WebhookType.INCOMING_ETHER: { + const { address, chainId, txHash, value } = payload.data + + // TODO: Native currency + title = `Incoming Ether` + // TODO: Parse value + body = `Safe ${address} on chain ${chainId} received ${value} Ether in transaction ${txHash}.` + + break + } + case WebhookType.OUTGOING_ETHER: { + const { address, chainId, txHash, value } = payload.data + + // TODO: Native currency + title = `Outgoing Ether` + body = `Safe ${address} on chain ${chainId} sent ${value} Ether in transaction ${txHash}.` + + break + } + case WebhookType.INCOMING_TOKEN: { + const { address, chainId, tokenAddress, txHash, value } = payload.data + + // TODO: Parse value and get token symbol + title = `Incoming Token` + body = `Safe ${address} on chain ${chainId} received ${value} Token in transaction ${txHash}.` + + break + } + case WebhookType.OUTGOING_TOKEN: { + const { address, chainId, tokenAddress, txHash, value } = payload.data + + // TODO: Parse value and get token symbol + title = `Outgoing Token` + body = `Safe ${address} on chain ${chainId} sent ${value} Token in transaction ${txHash}.` + + break + } + case WebhookType.SAFE_CREATED: { + const { address, chainId, txHash, blockNumber } = payload.data + + title = `Safe created` + body = `Safe ${address} on chain ${chainId} was created in transaction ${txHash} in block ${blockNumber}.` + + break + } + case WebhookType.MODULE_TRANSACTION: { + const { address, chainId, module, txHash } = payload.data + + title = `Module transaction` + body = `Safe ${address} on chain ${chainId} executed a module transaction ${txHash} from module ${module}.` + + break + } + case WebhookType.CONFIRMATION_REQUEST: { + const { address, chainId, safeTxHash } = payload.data + + title = `Confirmation request` + body = `Safe ${address} on chain ${chainId} has a new confirmation request for transaction ${safeTxHash}.` + + break + } + } + } + + if (title) { + self.registration.showNotification(title, { + body, + icon: image, + }) + } +}) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 849489e8d7..817342d215 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -4,13 +4,12 @@ import { getToken, getMessaging } from 'firebase/messaging' import type { ReactElement } from 'react' import packageJson from '../../../../package.json' -import { FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' +import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useCurrentChain } from '@/hooks/useChains' import EthHashInfo from '@/components/common/EthHashInfo' -import { getFirebaseSwRegistrationPath } from '@/services/firebase' const NOTIFICATIONS_LS_REGISTRATION_KEY = 'firebaseCloudMessaging' @@ -34,8 +33,7 @@ type RegisterDeviceDto = { } const getFirebaseToken = async () => { - const firebaseSwPath = getFirebaseSwRegistrationPath() - const swRegistration = await navigator.serviceWorker.getRegistration(firebaseSwPath) + const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) // Get token const messaging = getMessaging() diff --git a/src/config/constants.ts b/src/config/constants.ts index 67163ff844..1b5fdde94f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -56,6 +56,7 @@ export const FIREBASE_MESSAGING_SENDER_ID = process.env.NEXT_PUBLIC_FIREBASE_MES export const FIREBASE_APP_ID = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || '' export const FIREBASE_MEASUREMENT_ID = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID || '' export const FIREBASE_VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY || '' +export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' // Tenderly - API docs: https://www.notion.so/Simulate-API-Documentation-6f7009fe6d1a48c999ffeb7941efc104 export const TENDERLY_SIMULATE_ENDPOINT_URL = process.env.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL || '' diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index 418357e8c9..dec501e77f 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -4,7 +4,17 @@ import { getMessaging, onMessage } from 'firebase/messaging' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' -import { FIREBASE_CONFIG, getFirebaseSwRegistrationPath } from '@/services/firebase' +import { + FIREBASE_API_KEY, + FIREBASE_APP_ID, + FIREBASE_AUTH_DOMAIN, + FIREBASE_DATABASE_URL, + FIREBASE_MEASUREMENT_ID, + FIREBASE_MESSAGING_SENDER_ID, + FIREBASE_MESSAGING_SW_PATH, + FIREBASE_PROJECT_ID, + FIREBASE_STORAGE_BUCKET, +} from '@/config/constants' export const useFirebaseNotifications = (): null => { const dispatch = useAppDispatch() @@ -16,11 +26,7 @@ export const useFirebaseNotifications = (): null => { } const registerFirebaseSw = () => { - // Firebase normally registers a service worker when calling `getToken` - // but we register it manually to pass custom config from the env - const serviceWorkerPath = getFirebaseSwRegistrationPath() - - navigator.serviceWorker.register(serviceWorkerPath).catch(() => null) + navigator.serviceWorker.register(FIREBASE_MESSAGING_SW_PATH).catch(() => null) } window.addEventListener('load', registerFirebaseSw) @@ -37,7 +43,17 @@ export const useFirebaseNotifications = (): null => { } // TODO: Should this be added to the privacy policy? - const _app = initializeApp(FIREBASE_CONFIG) + const _app = initializeApp({ + apiKey: FIREBASE_API_KEY, + authDomain: FIREBASE_AUTH_DOMAIN, + databaseURL: FIREBASE_DATABASE_URL, + projectId: FIREBASE_PROJECT_ID, + storageBucket: FIREBASE_STORAGE_BUCKET, + messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, + appId: FIREBASE_APP_ID, + measurementId: FIREBASE_MEASUREMENT_ID, + }) + const messaging = getMessaging(_app) const unsubscribe = onMessage(messaging, (payload) => { diff --git a/src/services/firebase.ts b/src/services/firebase.ts deleted file mode 100644 index 9863142a98..0000000000 --- a/src/services/firebase.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - FIREBASE_API_KEY, - FIREBASE_AUTH_DOMAIN, - FIREBASE_DATABASE_URL, - FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET, - FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_APP_ID, - FIREBASE_MEASUREMENT_ID, -} from '@/config/constants' - -// TODO: Add these and VAPID_KEY to README.md -export const FIREBASE_CONFIG = { - apiKey: FIREBASE_API_KEY, - authDomain: FIREBASE_AUTH_DOMAIN, - databaseURL: FIREBASE_DATABASE_URL, - projectId: FIREBASE_PROJECT_ID, - storageBucket: FIREBASE_STORAGE_BUCKET, - messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, - appId: FIREBASE_APP_ID, - measurementId: FIREBASE_MEASUREMENT_ID, -} - -export const FIREBASE_SW_PATH = '/firebase-messaging-sw.js' - -export const getFirebaseSwRegistrationPath = (): string => { - const config = new URLSearchParams(FIREBASE_CONFIG).toString() - - // Service workers don't conflict as they are registered with params - return `${FIREBASE_SW_PATH}?${config}` -} diff --git a/yarn.lock b/yarn.lock index 8cfbbd9e09..3677816997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,6 +15,23 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + "@apocentre/alias-sampling@^0.5.3": version "0.5.3" resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" @@ -27,11 +44,45 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" + integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== + dependencies: + "@babel/highlight" "^7.22.10" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== +"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + +"@babel/core@^7.11.1": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" + integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.10" + "@babel/parser" "^7.22.10" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.1" + "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.19.6": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" @@ -62,6 +113,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" + integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== + dependencies: + "@babel/types" "^7.22.10" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -69,6 +130,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" @@ -77,6 +145,13 @@ "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz#573e735937e99ea75ea30788b57eb52fab7468c9" + integrity sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ== + dependencies: + "@babel/types" "^7.22.10" + "@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" @@ -87,6 +162,17 @@ browserslist "^4.21.3" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" + integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.2", "@babel/helper-create-class-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz#327154eedfb12e977baa4ecc72e5806720a85a06" @@ -100,6 +186,21 @@ "@babel/helper-replace-supers" "^7.19.1" "@babel/helper-split-export-declaration" "^7.18.6" +"@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3" + integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca" @@ -108,6 +209,15 @@ "@babel/helper-annotate-as-pure" "^7.18.6" regexpu-core "^5.2.1" +"@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" + integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" @@ -120,11 +230,27 @@ resolve "^1.14.2" semver "^6.1.2" +"@babel/helper-define-polyfill-provider@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" + integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -140,6 +266,14 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -147,6 +281,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" @@ -154,6 +295,20 @@ dependencies: "@babel/types" "^7.18.9" +"@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" + integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" @@ -175,6 +330,17 @@ "@babel/traverse" "^7.20.1" "@babel/types" "^7.20.2" +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -182,11 +348,23 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" @@ -197,6 +375,15 @@ "@babel/helper-wrap-function" "^7.18.9" "@babel/types" "^7.18.9" +"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" + integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.9" + "@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz#e1592a9b4b368aa6bdb8784a711e0bcbf0612b78" @@ -208,6 +395,15 @@ "@babel/traverse" "^7.19.1" "@babel/types" "^7.19.0" +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" + integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-simple-access@^7.19.4", "@babel/helper-simple-access@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" @@ -215,6 +411,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.18.9": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" @@ -222,6 +425,13 @@ dependencies: "@babel/types" "^7.20.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -229,21 +439,43 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + "@babel/helper-wrap-function@^7.18.9": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" @@ -254,6 +486,15 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" +"@babel/helper-wrap-function@^7.22.9": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz#d845e043880ed0b8c18bd194a12005cb16d2f614" + integrity sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.10" + "@babel/helpers@^7.20.5": version "7.20.6" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" @@ -263,6 +504,15 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" +"@babel/helpers@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" + integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -272,11 +522,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" + integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== +"@babel/parser@^7.22.10", "@babel/parser@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" + integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -284,6 +548,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" + integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" @@ -293,6 +564,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" "@babel/plugin-proposal-optional-chaining" "^7.18.9" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" + integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-async-generator-functions@^7.20.1": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz#352f02baa5d69f4e7529bdac39aaa02d41146af9" @@ -404,6 +684,11 @@ "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + "@babel/plugin-proposal-private-property-in-object@^7.18.6": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135" @@ -471,7 +756,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -555,6 +854,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-arrow-functions@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" @@ -562,6 +869,23 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8" + integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-transform-async-to-generator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" @@ -571,6 +895,15 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-remap-async-to-generator" "^7.18.6" +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" @@ -578,6 +911,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-block-scoping@^7.20.2": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.5.tgz#401215f9dc13dc5262940e2e527c9536b3d7f237" @@ -585,6 +925,30 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-transform-block-scoping@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" + integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba" + integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-transform-classes@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz#c0033cf1916ccf78202d04be4281d161f6709bb2" @@ -600,6 +964,21 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" + integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" @@ -607,6 +986,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/plugin-transform-destructuring@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz#c23741cfa44ddd35f5e53896e88c75331b8b2792" @@ -614,6 +1001,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-transform-destructuring@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" + integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" @@ -622,6 +1016,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-duplicate-keys@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" @@ -629,6 +1031,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e" + integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-exponentiation-operator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" @@ -637,6 +1054,22 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b" + integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-transform-for-of@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" @@ -644,6 +1077,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-for-of@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" + integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" @@ -653,6 +1093,23 @@ "@babel/helper-function-name" "^7.18.9" "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0" + integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-transform-literals@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" @@ -660,6 +1117,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c" + integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-transform-member-expression-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" @@ -667,6 +1139,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-modules-amd@^7.19.6": version "7.19.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz#aca391801ae55d19c4d8d2ebfeaa33df5f2a2cbd" @@ -675,6 +1154,14 @@ "@babel/helper-module-transforms" "^7.19.6" "@babel/helper-plugin-utils" "^7.19.0" +"@babel/plugin-transform-modules-amd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" + integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-modules-commonjs@^7.19.6": version "7.19.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" @@ -684,6 +1171,15 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-simple-access" "^7.19.4" +"@babel/plugin-transform-modules-commonjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" + integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/plugin-transform-modules-systemjs@^7.19.6": version "7.19.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz#59e2a84064b5736a4471b1aa7b13d4431d327e0d" @@ -694,6 +1190,16 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-identifier" "^7.19.1" +"@babel/plugin-transform-modules-systemjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496" + integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/plugin-transform-modules-umd@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" @@ -702,6 +1208,14 @@ "@babel/helper-module-transforms" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" @@ -710,6 +1224,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.20.5" "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-new-target@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" @@ -717,6 +1239,40 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381" + integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58" + integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1" + integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-object-super@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" @@ -725,6 +1281,31 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-replace-supers" "^7.18.6" +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333" + integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a" + integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-transform-parameters@^7.20.1": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.5.tgz#f8f9186c681d10c3de7620c916156d893c8a019e" @@ -732,6 +1313,31 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" +"@babel/plugin-transform-parameters@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" + integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32" + integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-transform-property-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" @@ -739,6 +1345,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-react-constant-elements@^7.18.12": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.20.2.tgz#3f02c784e0b711970d7d8ccc96c4359d64e27ac7" @@ -787,6 +1400,14 @@ "@babel/helper-plugin-utils" "^7.20.2" regenerator-transform "^0.15.1" +"@babel/plugin-transform-regenerator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" + integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.2" + "@babel/plugin-transform-reserved-words@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" @@ -794,6 +1415,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-runtime@^7.5.5": version "7.19.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz#9d2a9dbf4e12644d6f46e5e75bfbf02b5d6e9194" @@ -813,6 +1441,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-spread@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" @@ -821,6 +1456,14 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-sticky-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" @@ -828,6 +1471,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-template-literals@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" @@ -835,6 +1485,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-typeof-symbol@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" @@ -842,6 +1499,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-typescript@^7.18.6": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz#91515527b376fc122ba83b13d70b01af8fe98f3f" @@ -858,6 +1522,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" +"@babel/plugin-transform-unicode-escapes@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" + integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-unicode-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" @@ -866,6 +1545,108 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.11.0": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.10.tgz#3263b9fe2c8823d191d28e61eac60a79f9ce8a0f" + integrity sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.10" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.10" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.5" + "@babel/plugin-transform-classes" "^7.22.6" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.10" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.5" + "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.5" + "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" + "@babel/plugin-transform-numeric-separator" "^7.22.5" + "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.10" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.22.10" + babel-plugin-polyfill-corejs2 "^0.4.5" + babel-plugin-polyfill-corejs3 "^0.8.3" + babel-plugin-polyfill-regenerator "^0.5.2" + core-js-compat "^3.31.0" + semver "^6.3.1" + "@babel/preset-env@^7.19.4": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" @@ -947,6 +1728,15 @@ core-js-compat "^3.25.1" semver "^6.3.0" +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + "@babel/preset-modules@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" @@ -979,6 +1769,11 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-typescript" "^7.18.6" +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + "@babel/runtime-corejs3@^7.10.2": version "7.20.6" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz#63dae945963539ab0ad578efbf3eff271e7067ae" @@ -994,6 +1789,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.11.2": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.21.0": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" @@ -1017,6 +1819,15 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.7.2": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" @@ -1033,6 +1844,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" + integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== + dependencies: + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.10" + "@babel/types" "^7.22.10" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" @@ -1042,6 +1869,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.10", "@babel/types@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" + integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2834,6 +3670,15 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" @@ -2848,16 +3693,34 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -2874,6 +3737,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@keystonehq/base-eth-keyring@^0.6.4": version "0.6.4" resolved "https://registry.yarnpkg.com/@keystonehq/base-eth-keyring/-/base-eth-keyring-0.6.4.tgz#16a8b280861a70d096444bbeba236c0001d709d9" @@ -3537,6 +4408,43 @@ redux-thunk "^2.4.2" reselect "^4.1.8" +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + "@rushstack/eslint-patch@^1.1.3": version "1.2.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" @@ -3933,6 +4841,16 @@ "@stablelib/random" "^1.0.2" "@stablelib/wipe" "^1.0.1" +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + "@svgr/babel-plugin-add-jsx-attribute@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" @@ -4380,6 +5298,11 @@ dependencies: "@types/node" "*" +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + "@types/ethereum-protocol@*", "@types/ethereum-protocol@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.2.tgz#e765d4c6f4b5ebe906932bd20333e307c56a9bc7" @@ -4636,6 +5559,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -5438,6 +6368,11 @@ acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" @@ -5482,6 +6417,16 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.6.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -5703,7 +6648,7 @@ async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0: dependencies: lodash "^4.17.14" -async@^3.2.0: +async@^3.2.0, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -5814,6 +6759,15 @@ babel-plugin-polyfill-corejs2@^0.3.3: "@babel/helper-define-polyfill-provider" "^0.3.3" semver "^6.1.1" +babel-plugin-polyfill-corejs2@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" + integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.2" + semver "^6.3.1" + babel-plugin-polyfill-corejs3@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" @@ -5822,6 +6776,14 @@ babel-plugin-polyfill-corejs3@^0.6.0: "@babel/helper-define-polyfill-provider" "^0.3.3" core-js-compat "^3.25.1" +babel-plugin-polyfill-corejs3@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" + integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + core-js-compat "^3.31.0" + babel-plugin-polyfill-regenerator@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" @@ -5829,6 +6791,13 @@ babel-plugin-polyfill-regenerator@^0.4.1: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.3" +babel-plugin-polyfill-regenerator@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" + integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -6066,6 +7035,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -6147,6 +7123,16 @@ browserslist@^4.21.3, browserslist@^4.21.4: node-releases "^2.0.6" update-browserslist-db "^1.0.9" +browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -6272,6 +7258,11 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +builtin-modules@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + bytebuffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" @@ -6340,6 +7331,11 @@ caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001400: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz" integrity sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A== +caniuse-lite@^1.0.30001517: + version "1.0.30001519" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" + integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -6374,7 +7370,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -6627,7 +7623,7 @@ command-line-usage@^6.1.0: table-layout "^1.0.2" typical "^5.2.0" -commander@^2.20.3: +commander@^2.20.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -6724,6 +7720,13 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.4" +core-js-compat@^3.31.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90" + integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw== + dependencies: + browserslist "^4.21.9" + core-js-pure@^3.25.1: version "3.26.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.26.1.tgz#653f4d7130c427820dcecd3168b594e8bb095a33" @@ -6873,6 +7876,11 @@ crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -7370,11 +8378,23 @@ eip55@^2.1.0: dependencies: keccak "^3.0.3" +ejs@^3.1.6: + version "3.1.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +electron-to-chromium@^1.4.477: + version "1.4.488" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz#442b1855f8c84fb1ed79f518985c65db94f64cc9" + integrity sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ== + elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.4.1, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -7862,6 +8882,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + estree-walker@^2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" @@ -8525,7 +9550,7 @@ fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -8604,6 +9629,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -8786,7 +9818,7 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.1.0: +fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -8862,6 +9894,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -9306,7 +10343,7 @@ idb@7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== -idb@7.1.1: +idb@7.1.1, idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== @@ -9561,6 +10598,11 @@ is-map@^2.0.1, is-map@^2.0.2: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-nan@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" @@ -9586,6 +10628,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -9614,6 +10661,11 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + is-set@^2.0.1, is-set@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" @@ -9770,6 +10822,16 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jake@^10.8.5: + version "10.8.7" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + jayson@^3.4.4: version "3.7.0" resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25" @@ -10194,6 +11256,15 @@ jest-watcher@^28.1.3: jest-util "^28.1.3" string-length "^4.0.1" +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest-worker@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" @@ -10353,7 +11424,12 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema@0.4.0: +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0, json-schema@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== @@ -10382,7 +11458,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.3, json5@^2.2.1, json5@^2.2.2: +json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -10413,6 +11489,11 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonpointer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + jsonschema@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" @@ -10697,6 +11778,11 @@ lodash.partition@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.partition/-/lodash.partition-4.6.0.tgz#a38e46b73469e0420b0da1212e66d414be364ba4" integrity sha512-35L3dSF3Q6V1w5j6V3NhNlQjzsRDC/pYKCTdYTmwqSib+Q8ponkAmt/PwEOq3EmI38DSCl+SkIVwLd+uSlVdrg== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash.uniqby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" @@ -10765,6 +11851,13 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -10789,6 +11882,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -10979,6 +12079,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6, minimist@~1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -11225,6 +12332,11 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + node-releases@^2.0.6: version "2.0.8" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" @@ -11602,7 +12714,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -11790,7 +12902,7 @@ prettier@^2.3.1, prettier@^2.7.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== -pretty-bytes@^5.6.0: +pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -12351,6 +13463,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -12358,6 +13475,13 @@ regenerator-transform@^0.15.1: dependencies: "@babel/runtime" "^7.8.4" +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -12384,6 +13508,18 @@ regexpu-core@^5.2.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + regjsgen@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" @@ -12434,6 +13570,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -12607,6 +13748,23 @@ rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4: dependencies: bn.js "^5.2.0" +rollup-plugin-terser@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.43.1: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + rpc-websockets@^7.5.0: version "7.5.0" resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748" @@ -12800,6 +13958,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2: version "7.5.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" @@ -12831,6 +13994,13 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + serve-static@1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" @@ -13003,6 +14173,11 @@ sonic-boom@^2.2.1: dependencies: atomic-sleep "^1.0.0" +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -13016,6 +14191,14 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -13031,6 +14214,18 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + spawn-sync@^1.0.15: version "1.0.15" resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" @@ -13140,7 +14335,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== @@ -13191,6 +14386,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -13215,6 +14419,11 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -13416,6 +14625,21 @@ tar@^4.0.2: safe-buffer "^5.2.1" yallist "^3.1.1" +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -13424,6 +14648,16 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +terser@^5.0.0: + version "5.19.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" + integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -13565,6 +14799,13 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + tr46@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" @@ -13723,6 +14964,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -13875,6 +15121,13 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -13900,6 +15153,19 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-browserslist-db@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -14325,6 +15591,11 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -14345,6 +15616,14 @@ webpack-bundle-analyzer@4.7.0: sirv "^1.0.7" ws "^7.3.1" +webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + webrtc-adapter@^7.2.1: version "7.7.1" resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz#b2c227a6144983b35057df67bd984a7d4bfd17f1" @@ -14425,6 +15704,15 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -14504,6 +15792,175 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" +workbox-background-sync@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5" + integrity sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA== + dependencies: + idb "^7.0.1" + workbox-core "7.0.0" + +workbox-broadcast-update@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22" + integrity sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ== + dependencies: + workbox-core "7.0.0" + +workbox-build@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4" + integrity sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.11.1" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^11.2.1" + "@rollup/plugin-replace" "^2.4.1" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + rollup-plugin-terser "^7.0.0" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "7.0.0" + workbox-broadcast-update "7.0.0" + workbox-cacheable-response "7.0.0" + workbox-core "7.0.0" + workbox-expiration "7.0.0" + workbox-google-analytics "7.0.0" + workbox-navigation-preload "7.0.0" + workbox-precaching "7.0.0" + workbox-range-requests "7.0.0" + workbox-recipes "7.0.0" + workbox-routing "7.0.0" + workbox-strategies "7.0.0" + workbox-streams "7.0.0" + workbox-sw "7.0.0" + workbox-window "7.0.0" + +workbox-cacheable-response@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2" + integrity sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g== + dependencies: + workbox-core "7.0.0" + +workbox-core@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545" + integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ== + +workbox-expiration@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa" + integrity sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ== + dependencies: + idb "^7.0.1" + workbox-core "7.0.0" + +workbox-google-analytics@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a" + integrity sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg== + dependencies: + workbox-background-sync "7.0.0" + workbox-core "7.0.0" + workbox-routing "7.0.0" + workbox-strategies "7.0.0" + +workbox-navigation-preload@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c" + integrity sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA== + dependencies: + workbox-core "7.0.0" + +workbox-precaching@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03" + integrity sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA== + dependencies: + workbox-core "7.0.0" + workbox-routing "7.0.0" + workbox-strategies "7.0.0" + +workbox-range-requests@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed" + integrity sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ== + dependencies: + workbox-core "7.0.0" + +workbox-recipes@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514" + integrity sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww== + dependencies: + workbox-cacheable-response "7.0.0" + workbox-core "7.0.0" + workbox-expiration "7.0.0" + workbox-precaching "7.0.0" + workbox-routing "7.0.0" + workbox-strategies "7.0.0" + +workbox-routing@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302" + integrity sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA== + dependencies: + workbox-core "7.0.0" + +workbox-strategies@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382" + integrity sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA== + dependencies: + workbox-core "7.0.0" + +workbox-streams@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9" + integrity sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ== + dependencies: + workbox-core "7.0.0" + workbox-routing "7.0.0" + +workbox-sw@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86" + integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA== + +workbox-webpack-plugin@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55" + integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw== + dependencies: + fast-json-stable-stringify "^2.1.0" + pretty-bytes "^5.4.1" + upath "^1.2.0" + webpack-sources "^1.4.3" + workbox-build "7.0.0" + +workbox-window@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08" + integrity sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "7.0.0" + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -14652,7 +16109,7 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== -yallist@^3.0.0, yallist@^3.1.1: +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== From 2168cdf16f5c5862e4feebfa2fe1fbc9f8c4a889 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 9 Aug 2023 15:23:25 +0200 Subject: [PATCH 06/62] fix: improve notifications --- public/firebase-messaging-sw.ts | 229 ++--------------- src/hooks/useFirebaseNotifications.ts | 13 +- src/services/firebase.ts | 352 ++++++++++++++++++++++++++ 3 files changed, 381 insertions(+), 213 deletions(-) create mode 100644 src/services/firebase.ts diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 37f1cb7cf5..18a35e36d0 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -3,7 +3,8 @@ import { initializeApp } from 'firebase/app' import { onBackgroundMessage } from 'firebase/messaging/sw' import { getMessaging } from 'firebase/messaging/sw' -import type { MessagePayload } from 'firebase/messaging/sw' + +import { parseFirebaseNotification } from '@/services/firebase' // Default type of `self` is `WorkerGlobalScope & typeof globalThis` // https://github.com/microsoft/TypeScript/issues/14877 @@ -12,113 +13,20 @@ declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown } // Satisfy Workbox self.__WB_MANIFEST -// TODO: Remove those which aren't used for notifications -// https://github.com/safe-global/safe-transaction-service/blob/c562ef3a43f77f6d38e9ea704e1434394598aa30/safe_transaction_service/history/signals.py#L138 -enum WebhookType { - NEW_CONFIRMATION = 'NEW_CONFIRMATION', - EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', - PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', - INCOMING_ETHER = 'INCOMING_ETHER', - OUTGOING_ETHER = 'OUTGOING_ETHER', - INCOMING_TOKEN = 'INCOMING_TOKEN', - OUTGOING_TOKEN = 'OUTGOING_TOKEN', - SAFE_CREATED = 'SAFE_CREATED', - MODULE_TRANSACTION = 'MODULE_TRANSACTION', - CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook -} - -type NewConfirmationEvent = { - type: WebhookType.NEW_CONFIRMATION - chainId: string - address: string - owner: string - safeTxHash: string -} - -type ExecutedMultisigTransactionEvent = { - type: WebhookType.EXECUTED_MULTISIG_TRANSACTION - chainId: string - address: string - safeTxHash: string - failed: boolean - txHash: string -} - -type PendingMultisigTransactionEvent = { - type: WebhookType.PENDING_MULTISIG_TRANSACTION - chainId: string - address: string - safeTxHash: string -} - -type IncomingEtherEvent = { - type: WebhookType.INCOMING_ETHER - chainId: string - address: string - txHash: string - value: string -} - -type OutgoingEtherEvent = { - type: WebhookType.OUTGOING_ETHER - chainId: string - address: string - txHash: string - value: string -} - -type IncomingTokenEvent = { - type: WebhookType.INCOMING_TOKEN - chainId: string - address: string - tokenAddress: string - txHash: string - value?: string // If ERC-20 token -} - -type OutgoingTokenEvent = { - type: WebhookType.OUTGOING_TOKEN - chainId: string - address: string - tokenAddress: string - txHash: string - value?: string // If ERC-20 token -} - -type SafeCreatedEvent = { - type: WebhookType.SAFE_CREATED - chainId: string - address: string - txHash: string - blockNumber: string -} - -type ModuleTransactionEvent = { - type: WebhookType.MODULE_TRANSACTION - chainId: string - address: string - module: string - txHash: string -} +// Must be called before `onBackgroundMessage` as Firebase embeds a `notificationclick` listener +self.addEventListener( + 'notificationclick', + (event) => { + event.notification.close() -type ConfirmationRequest = { - type: WebhookType.CONFIRMATION_REQUEST - chainId: string - address: string - safeTxHash: string -} + const link = event.notification.tag -type WebhookEvent = - | NewConfirmationEvent - | ExecutedMultisigTransactionEvent - | PendingMultisigTransactionEvent - | IncomingEtherEvent - | OutgoingEtherEvent - | IncomingTokenEvent - | OutgoingTokenEvent - | SafeCreatedEvent - | ModuleTransactionEvent - | ConfirmationRequest + if (link) { + self.clients.openWindow(link) + } + }, + false, +) const app = initializeApp({ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, @@ -133,114 +41,15 @@ const app = initializeApp({ const messaging = getMessaging(app) -const isWebhookEvent = (data: T): data is T & { ['data']: WebhookEvent } => { - return Object.values(WebhookType).some((type) => type === data?.type) -} - -onBackgroundMessage(messaging, (payload) => { - // TODO: Add default values - let title, body, image - - if (payload.notification) { - ;({ title, body, image } = payload.notification) - } - - if (isWebhookEvent(payload.data)) { - switch (payload.data.type) { - case WebhookType.NEW_CONFIRMATION: { - const { address, chainId, owner, safeTxHash } = payload.data - - title = `New confirmation for ${safeTxHash}` - body = `Safe ${address} on chain ${chainId} has a new confirmation from ${owner}.` - - break - } - case WebhookType.EXECUTED_MULTISIG_TRANSACTION: { - const { address, chainId, failed, safeTxHash, txHash } = payload.data - - title = failed ? `Transaction ${safeTxHash} failed` : `Transaction ${safeTxHash} executed` - body = failed - ? `Safe ${address} on chain ${chainId} failed to execute transaction ${txHash}.` - : `Safe ${address} on chain ${chainId} executed transaction ${txHash}.` - - break - } - case WebhookType.PENDING_MULTISIG_TRANSACTION: { - const { address, chainId, safeTxHash } = payload.data - - title = `New pending transaction for ${safeTxHash}` - body = `Safe ${address} on chain ${chainId} has a new pending transaction ${safeTxHash}.` - - break - } - case WebhookType.INCOMING_ETHER: { - const { address, chainId, txHash, value } = payload.data - - // TODO: Native currency - title = `Incoming Ether` - // TODO: Parse value - body = `Safe ${address} on chain ${chainId} received ${value} Ether in transaction ${txHash}.` - - break - } - case WebhookType.OUTGOING_ETHER: { - const { address, chainId, txHash, value } = payload.data - - // TODO: Native currency - title = `Outgoing Ether` - body = `Safe ${address} on chain ${chainId} sent ${value} Ether in transaction ${txHash}.` - - break - } - case WebhookType.INCOMING_TOKEN: { - const { address, chainId, tokenAddress, txHash, value } = payload.data - - // TODO: Parse value and get token symbol - title = `Incoming Token` - body = `Safe ${address} on chain ${chainId} received ${value} Token in transaction ${txHash}.` - - break - } - case WebhookType.OUTGOING_TOKEN: { - const { address, chainId, tokenAddress, txHash, value } = payload.data - - // TODO: Parse value and get token symbol - title = `Outgoing Token` - body = `Safe ${address} on chain ${chainId} sent ${value} Token in transaction ${txHash}.` - - break - } - case WebhookType.SAFE_CREATED: { - const { address, chainId, txHash, blockNumber } = payload.data - - title = `Safe created` - body = `Safe ${address} on chain ${chainId} was created in transaction ${txHash} in block ${blockNumber}.` - - break - } - case WebhookType.MODULE_TRANSACTION: { - const { address, chainId, module, txHash } = payload.data - - title = `Module transaction` - body = `Safe ${address} on chain ${chainId} executed a module transaction ${txHash} from module ${module}.` - - break - } - case WebhookType.CONFIRMATION_REQUEST: { - const { address, chainId, safeTxHash } = payload.data - - title = `Confirmation request` - body = `Safe ${address} on chain ${chainId} has a new confirmation request for transaction ${safeTxHash}.` - - break - } - } - } +onBackgroundMessage(messaging, async (payload) => { + const { title, body, image, link } = await parseFirebaseNotification(payload) if (title) { self.registration.showNotification(title, { + icon: '/images/safe-logo-green.png', body, - icon: image, + image, + tag: link, }) } }) diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index dec501e77f..da1a42e9ae 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -15,6 +15,7 @@ import { FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, } from '@/config/constants' +import { parseFirebaseNotification } from '@/services/firebase' export const useFirebaseNotifications = (): null => { const dispatch = useAppDispatch() @@ -56,11 +57,17 @@ export const useFirebaseNotifications = (): null => { const messaging = getMessaging(_app) - const unsubscribe = onMessage(messaging, (payload) => { + const unsubscribe = onMessage(messaging, async (payload) => { + const { title, body } = await parseFirebaseNotification(payload) + + if (!title) { + return + } + dispatch( showNotification({ - message: payload.notification?.title || '', - detailedMessage: payload.notification?.body, + message: title, + detailedMessage: body, groupKey: payload.messageId, variant: 'info', }), diff --git a/src/services/firebase.ts b/src/services/firebase.ts new file mode 100644 index 0000000000..0eb3e7fa1d --- /dev/null +++ b/src/services/firebase.ts @@ -0,0 +1,352 @@ +import { parseUnits } from 'ethers/lib/utils' +import type { MessagePayload } from 'firebase/messaging/sw' +import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' + +import { shortenAddress } from '@/utils/formatters' +import { AppRoutes } from '@/config/routes' + +// Types + +enum WebhookType { + NEW_CONFIRMATION = 'NEW_CONFIRMATION', + EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', + PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', + INCOMING_ETHER = 'INCOMING_ETHER', + OUTGOING_ETHER = 'OUTGOING_ETHER', + INCOMING_TOKEN = 'INCOMING_TOKEN', + OUTGOING_TOKEN = 'OUTGOING_TOKEN', + SAFE_CREATED = 'SAFE_CREATED', + MODULE_TRANSACTION = 'MODULE_TRANSACTION', + CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook +} + +type NewConfirmationEvent = { + type: WebhookType.NEW_CONFIRMATION + chainId: string + address: string + owner: string + safeTxHash: string +} + +type ExecutedMultisigTransactionEvent = { + type: WebhookType.EXECUTED_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string + failed: boolean + txHash: string +} + +type PendingMultisigTransactionEvent = { + type: WebhookType.PENDING_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string +} + +type IncomingEtherEvent = { + type: WebhookType.INCOMING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +type OutgoingEtherEvent = { + type: WebhookType.OUTGOING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +type IncomingTokenEvent = { + type: WebhookType.INCOMING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +type OutgoingTokenEvent = { + type: WebhookType.OUTGOING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +type SafeCreatedEvent = { + type: WebhookType.SAFE_CREATED + chainId: string + address: string + txHash: string + blockNumber: string +} + +type ModuleTransactionEvent = { + type: WebhookType.MODULE_TRANSACTION + chainId: string + address: string + module: string + txHash: string +} + +type ConfirmationRequest = { + type: WebhookType.CONFIRMATION_REQUEST + chainId: string + address: string + safeTxHash: string +} + +type WebhookEvent = + | NewConfirmationEvent + | ExecutedMultisigTransactionEvent + | PendingMultisigTransactionEvent + | IncomingEtherEvent + | OutgoingEtherEvent + | IncomingTokenEvent + | OutgoingTokenEvent + | SafeCreatedEvent + | ModuleTransactionEvent + | ConfirmationRequest + +// Services + +// XHR is not supported in service worker so we can't use the SDK + +const BASE_URL = 'https://safe-client.safe.global' + +const getChains = async (): Promise => { + const ENDPOINT = `${BASE_URL}/v1/chains` + + const response = await fetch(ENDPOINT) + + if (response.ok) { + return response.json() + } +} + +const getBalances = async (chainId: string, safeAddress: string): Promise => { + const DEFAULT_CURRENCY = 'USD' + const ENDPOINT = `${BASE_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` + + const response = await fetch(ENDPOINT) + + if (response.ok) { + return response.json() + } +} + +const getToken = async ( + chainId: string, + safeAddress: string, + tokenAddress: string, +): Promise => { + let balances: SafeBalanceResponse | undefined + + try { + balances = await getBalances(chainId, safeAddress) + } catch {} + + return balances?.items.find((token) => token.tokenInfo.address === tokenAddress) +} + +// Helpers + +const isWebhookEvent = (data: T): data is T & { ['data']: WebhookEvent } => { + return Object.values(WebhookType).some((type) => type === data?.type) +} + +const getChain = (chainId: string, chains?: Array): ChainInfo | undefined => { + return chains?.find((chain) => chain.chainId === chainId) +} + +const getLink = (address: string, path: string, chain?: ChainInfo) => { + if (!chain) { + return path + } + + return `https://app.safe.global${path}?safe=${chain.shortName}:${address}` +} + +// Main + +export const parseFirebaseNotification = async ( + payload: MessagePayload, +): Promise< + { + title?: string + link: string + } & NotificationOptions +> => { + // TODO: Add icon + let title, body, image + let link = AppRoutes.index + + if (payload.notification) { + ;({ title, body, image } = payload.notification) + } + + if (isWebhookEvent(payload.data)) { + let chains: Array | undefined + + try { + const response = await getChains() + chains = response?.results + } catch {} + + switch (payload.data.type) { + case WebhookType.NEW_CONFIRMATION: { + const { address, chainId, owner, safeTxHash } = payload.data + const chain = getChain(chainId, chains) + + title = `New confirmation` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } has a new confirmation from ${shortenAddress(owner)} on transaction ${shortenAddress(safeTxHash)}.` + + link = getLink(address, AppRoutes.transactions.queue, chain) + + break + } + case WebhookType.EXECUTED_MULTISIG_TRANSACTION: { + const { address, chainId, failed, safeTxHash, txHash } = payload.data + const chain = getChain(chainId, chains) + + if (failed === 'true') { + title = `Transaction failed` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } failed to execute transaction ${shortenAddress(txHash ?? safeTxHash)}.` + + link = getLink(address, AppRoutes.transactions.queue, chain) + } else { + title = `Transaction executed` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } executed transaction ${shortenAddress(txHash ?? safeTxHash)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + } + + break + } + case WebhookType.PENDING_MULTISIG_TRANSACTION: { + const { address, chainId, safeTxHash } = payload.data + const chain = getChain(chainId, chains) + + title = `New pending transaction` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } has a new pending transaction ${shortenAddress(safeTxHash)}.` + + link = getLink(address, AppRoutes.transactions.queue, chain) + + break + } + case WebhookType.INCOMING_ETHER: { + const { address, chainId, txHash, value } = payload.data + const chain = getChain(chainId, chains) + + title = `Incoming ${chain?.nativeCurrency?.symbol || 'ETH'}` + + body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} received ${ + chain ? parseUnits(value, chain.nativeCurrency.decimals) : value + } Ether in transaction ${shortenAddress(txHash)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + + break + } + case WebhookType.OUTGOING_ETHER: { + const { address, chainId, txHash, value } = payload.data + const chain = getChain(chainId, chains) + + title = `Outgoing ${chain?.nativeCurrency?.symbol || 'ETH'}` + + body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} sent ${ + chain ? parseUnits(value, chain.nativeCurrency.decimals) : value + } Ether in transaction ${shortenAddress(txHash)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + + break + } + case WebhookType.INCOMING_TOKEN: { + const { address, chainId, tokenAddress, txHash, value } = payload.data + const chain = getChain(chainId, chains) + + const token = await getToken(chainId, address, tokenAddress) + + title = `Incoming ${token?.tokenInfo?.name || 'Token'}` + + body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} received ${ + token ? parseUnits(value, token.tokenInfo.decimals) : value + } Token in transaction ${shortenAddress(txHash)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + + break + } + case WebhookType.OUTGOING_TOKEN: { + const { address, chainId, tokenAddress, txHash, value } = payload.data + const chain = getChain(chainId, chains) + + const token = await getToken(chainId, address, tokenAddress) + + title = `Outgoing ${token?.tokenInfo?.name || 'Token'}` + + body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} sent ${ + token ? parseUnits(value, token.tokenInfo.decimals) : value + } Token in transaction ${shortenAddress(txHash)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + + break + } + case WebhookType.SAFE_CREATED: { + // Notifications are subscribed to per Safe so we would only show this notification + // if the user was subscribed to a pre-determined address + + break + } + case WebhookType.MODULE_TRANSACTION: { + const { address, chainId, module, txHash } = payload.data + const chain = getChain(chainId, chains) + + title = `Module transaction` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } executed a module transaction ${shortenAddress(txHash)} from module ${shortenAddress(module)}.` + + link = getLink(address, AppRoutes.transactions.history, chain) + + break + } + case WebhookType.CONFIRMATION_REQUEST: { + const { address, chainId, safeTxHash } = payload.data + const chain = getChain(chainId, chains) + + title = `Confirmation request` + + body = `Safe ${shortenAddress(address)} on chain ${ + chain?.chainName ?? chainId + } has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.` + + link = getLink(address, AppRoutes.transactions.queue, chain) + + break + } + } + } + + return { title, body, image, link } +} From 70ee4cacd16d89b236b18970ea518e0663ff415f Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 9 Aug 2023 19:44:57 +0200 Subject: [PATCH 07/62] refactor: separate types + add test --- public/firebase-messaging-sw.ts | 19 +- src/hooks/useFirebaseNotifications.ts | 8 +- src/services/firebase.ts | 352 ----------- src/services/firebase/index.test.ts | 820 ++++++++++++++++++++++++++ src/services/firebase/index.ts | 220 +++++++ src/services/firebase/webhooks.ts | 111 ++++ 6 files changed, 1166 insertions(+), 364 deletions(-) delete mode 100644 src/services/firebase.ts create mode 100644 src/services/firebase/index.test.ts create mode 100644 src/services/firebase/index.ts create mode 100644 src/services/firebase/webhooks.ts diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 18a35e36d0..4321c50b16 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -42,14 +42,17 @@ const app = initializeApp({ const messaging = getMessaging(app) onBackgroundMessage(messaging, async (payload) => { - const { title, body, image, link } = await parseFirebaseNotification(payload) - - if (title) { - self.registration.showNotification(title, { - icon: '/images/safe-logo-green.png', - body, - image, - tag: link, + const ICON_PATH = '/images/safe-logo-green.png' + const DEFAULT_LINK = 'https://app.safe.global' + + const notification = await parseFirebaseNotification(payload) + + if (notification) { + self.registration.showNotification(notification.title, { + icon: ICON_PATH, + body: notification.body, + image: notification.image, + tag: notification.link ?? DEFAULT_LINK, }) } }) diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index da1a42e9ae..ce21b78d8e 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -58,16 +58,16 @@ export const useFirebaseNotifications = (): null => { const messaging = getMessaging(_app) const unsubscribe = onMessage(messaging, async (payload) => { - const { title, body } = await parseFirebaseNotification(payload) + const notification = await parseFirebaseNotification(payload) - if (!title) { + if (!notification) { return } dispatch( showNotification({ - message: title, - detailedMessage: body, + message: notification.title, + detailedMessage: notification.body, groupKey: payload.messageId, variant: 'info', }), diff --git a/src/services/firebase.ts b/src/services/firebase.ts deleted file mode 100644 index 0eb3e7fa1d..0000000000 --- a/src/services/firebase.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { parseUnits } from 'ethers/lib/utils' -import type { MessagePayload } from 'firebase/messaging/sw' -import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' - -import { shortenAddress } from '@/utils/formatters' -import { AppRoutes } from '@/config/routes' - -// Types - -enum WebhookType { - NEW_CONFIRMATION = 'NEW_CONFIRMATION', - EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', - PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', - INCOMING_ETHER = 'INCOMING_ETHER', - OUTGOING_ETHER = 'OUTGOING_ETHER', - INCOMING_TOKEN = 'INCOMING_TOKEN', - OUTGOING_TOKEN = 'OUTGOING_TOKEN', - SAFE_CREATED = 'SAFE_CREATED', - MODULE_TRANSACTION = 'MODULE_TRANSACTION', - CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook -} - -type NewConfirmationEvent = { - type: WebhookType.NEW_CONFIRMATION - chainId: string - address: string - owner: string - safeTxHash: string -} - -type ExecutedMultisigTransactionEvent = { - type: WebhookType.EXECUTED_MULTISIG_TRANSACTION - chainId: string - address: string - safeTxHash: string - failed: boolean - txHash: string -} - -type PendingMultisigTransactionEvent = { - type: WebhookType.PENDING_MULTISIG_TRANSACTION - chainId: string - address: string - safeTxHash: string -} - -type IncomingEtherEvent = { - type: WebhookType.INCOMING_ETHER - chainId: string - address: string - txHash: string - value: string -} - -type OutgoingEtherEvent = { - type: WebhookType.OUTGOING_ETHER - chainId: string - address: string - txHash: string - value: string -} - -type IncomingTokenEvent = { - type: WebhookType.INCOMING_TOKEN - chainId: string - address: string - tokenAddress: string - txHash: string - value?: string // If ERC-20 token -} - -type OutgoingTokenEvent = { - type: WebhookType.OUTGOING_TOKEN - chainId: string - address: string - tokenAddress: string - txHash: string - value?: string // If ERC-20 token -} - -type SafeCreatedEvent = { - type: WebhookType.SAFE_CREATED - chainId: string - address: string - txHash: string - blockNumber: string -} - -type ModuleTransactionEvent = { - type: WebhookType.MODULE_TRANSACTION - chainId: string - address: string - module: string - txHash: string -} - -type ConfirmationRequest = { - type: WebhookType.CONFIRMATION_REQUEST - chainId: string - address: string - safeTxHash: string -} - -type WebhookEvent = - | NewConfirmationEvent - | ExecutedMultisigTransactionEvent - | PendingMultisigTransactionEvent - | IncomingEtherEvent - | OutgoingEtherEvent - | IncomingTokenEvent - | OutgoingTokenEvent - | SafeCreatedEvent - | ModuleTransactionEvent - | ConfirmationRequest - -// Services - -// XHR is not supported in service worker so we can't use the SDK - -const BASE_URL = 'https://safe-client.safe.global' - -const getChains = async (): Promise => { - const ENDPOINT = `${BASE_URL}/v1/chains` - - const response = await fetch(ENDPOINT) - - if (response.ok) { - return response.json() - } -} - -const getBalances = async (chainId: string, safeAddress: string): Promise => { - const DEFAULT_CURRENCY = 'USD' - const ENDPOINT = `${BASE_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` - - const response = await fetch(ENDPOINT) - - if (response.ok) { - return response.json() - } -} - -const getToken = async ( - chainId: string, - safeAddress: string, - tokenAddress: string, -): Promise => { - let balances: SafeBalanceResponse | undefined - - try { - balances = await getBalances(chainId, safeAddress) - } catch {} - - return balances?.items.find((token) => token.tokenInfo.address === tokenAddress) -} - -// Helpers - -const isWebhookEvent = (data: T): data is T & { ['data']: WebhookEvent } => { - return Object.values(WebhookType).some((type) => type === data?.type) -} - -const getChain = (chainId: string, chains?: Array): ChainInfo | undefined => { - return chains?.find((chain) => chain.chainId === chainId) -} - -const getLink = (address: string, path: string, chain?: ChainInfo) => { - if (!chain) { - return path - } - - return `https://app.safe.global${path}?safe=${chain.shortName}:${address}` -} - -// Main - -export const parseFirebaseNotification = async ( - payload: MessagePayload, -): Promise< - { - title?: string - link: string - } & NotificationOptions -> => { - // TODO: Add icon - let title, body, image - let link = AppRoutes.index - - if (payload.notification) { - ;({ title, body, image } = payload.notification) - } - - if (isWebhookEvent(payload.data)) { - let chains: Array | undefined - - try { - const response = await getChains() - chains = response?.results - } catch {} - - switch (payload.data.type) { - case WebhookType.NEW_CONFIRMATION: { - const { address, chainId, owner, safeTxHash } = payload.data - const chain = getChain(chainId, chains) - - title = `New confirmation` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } has a new confirmation from ${shortenAddress(owner)} on transaction ${shortenAddress(safeTxHash)}.` - - link = getLink(address, AppRoutes.transactions.queue, chain) - - break - } - case WebhookType.EXECUTED_MULTISIG_TRANSACTION: { - const { address, chainId, failed, safeTxHash, txHash } = payload.data - const chain = getChain(chainId, chains) - - if (failed === 'true') { - title = `Transaction failed` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } failed to execute transaction ${shortenAddress(txHash ?? safeTxHash)}.` - - link = getLink(address, AppRoutes.transactions.queue, chain) - } else { - title = `Transaction executed` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } executed transaction ${shortenAddress(txHash ?? safeTxHash)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - } - - break - } - case WebhookType.PENDING_MULTISIG_TRANSACTION: { - const { address, chainId, safeTxHash } = payload.data - const chain = getChain(chainId, chains) - - title = `New pending transaction` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } has a new pending transaction ${shortenAddress(safeTxHash)}.` - - link = getLink(address, AppRoutes.transactions.queue, chain) - - break - } - case WebhookType.INCOMING_ETHER: { - const { address, chainId, txHash, value } = payload.data - const chain = getChain(chainId, chains) - - title = `Incoming ${chain?.nativeCurrency?.symbol || 'ETH'}` - - body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} received ${ - chain ? parseUnits(value, chain.nativeCurrency.decimals) : value - } Ether in transaction ${shortenAddress(txHash)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - - break - } - case WebhookType.OUTGOING_ETHER: { - const { address, chainId, txHash, value } = payload.data - const chain = getChain(chainId, chains) - - title = `Outgoing ${chain?.nativeCurrency?.symbol || 'ETH'}` - - body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} sent ${ - chain ? parseUnits(value, chain.nativeCurrency.decimals) : value - } Ether in transaction ${shortenAddress(txHash)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - - break - } - case WebhookType.INCOMING_TOKEN: { - const { address, chainId, tokenAddress, txHash, value } = payload.data - const chain = getChain(chainId, chains) - - const token = await getToken(chainId, address, tokenAddress) - - title = `Incoming ${token?.tokenInfo?.name || 'Token'}` - - body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} received ${ - token ? parseUnits(value, token.tokenInfo.decimals) : value - } Token in transaction ${shortenAddress(txHash)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - - break - } - case WebhookType.OUTGOING_TOKEN: { - const { address, chainId, tokenAddress, txHash, value } = payload.data - const chain = getChain(chainId, chains) - - const token = await getToken(chainId, address, tokenAddress) - - title = `Outgoing ${token?.tokenInfo?.name || 'Token'}` - - body = `Safe ${shortenAddress(address)} on chain ${chain?.chainName ?? chainId} sent ${ - token ? parseUnits(value, token.tokenInfo.decimals) : value - } Token in transaction ${shortenAddress(txHash)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - - break - } - case WebhookType.SAFE_CREATED: { - // Notifications are subscribed to per Safe so we would only show this notification - // if the user was subscribed to a pre-determined address - - break - } - case WebhookType.MODULE_TRANSACTION: { - const { address, chainId, module, txHash } = payload.data - const chain = getChain(chainId, chains) - - title = `Module transaction` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } executed a module transaction ${shortenAddress(txHash)} from module ${shortenAddress(module)}.` - - link = getLink(address, AppRoutes.transactions.history, chain) - - break - } - case WebhookType.CONFIRMATION_REQUEST: { - const { address, chainId, safeTxHash } = payload.data - const chain = getChain(chainId, chains) - - title = `Confirmation request` - - body = `Safe ${shortenAddress(address)} on chain ${ - chain?.chainName ?? chainId - } has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.` - - link = getLink(address, AppRoutes.transactions.queue, chain) - - break - } - } - } - - return { title, body, image, link } -} diff --git a/src/services/firebase/index.test.ts b/src/services/firebase/index.test.ts new file mode 100644 index 0000000000..c9f719abd8 --- /dev/null +++ b/src/services/firebase/index.test.ts @@ -0,0 +1,820 @@ +import { hexZeroPad } from 'ethers/lib/utils' +import type { ChainInfo, TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' + +import { _parseWebhookNotification } from '.' +import { WebhookType } from './webhooks' +import type { + ConfirmationRequestEvent, + ExecutedMultisigTransactionEvent, + IncomingEtherEvent, + IncomingTokenEvent, + ModuleTransactionEvent, + NewConfirmationEvent, + OutgoingEtherEvent, + OutgoingTokenEvent, + PendingMultisigTransactionEvent, + SafeCreatedEvent, +} from './webhooks' + +const setupFetchStub = (data: any) => (_url: string) => { + return Promise.resolve({ + json: () => Promise.resolve(data), + status: 200, + ok: true, + }) +} + +describe('parseWebhookNotification', () => { + beforeEach(() => { + global.fetch = jest.fn() + }) + + describe('should parse NEW_CONFIRMATION payloads', () => { + const payload: NewConfirmationEvent = { + type: WebhookType.NEW_CONFIRMATION, + chainId: '1', + address: hexZeroPad('0x1', 20), + owner: hexZeroPad('0x2', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'New confirmation', + body: 'Safe 0x0000...0001 on Mainnet has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'New confirmation', + body: 'Safe 0x0000...0001 on chain 1 has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse EXECUTED_MULTISIG_TRANSACTION payloads', () => { + const payload: Omit = { + type: WebhookType.EXECUTED_MULTISIG_TRANSACTION, + chainId: '1', + address: hexZeroPad('0x1', 20), + safeTxHash: hexZeroPad('0x3', 32), + txHash: hexZeroPad('0x4', 32), + } + + describe('successful transactions', () => { + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification({ + ...payload, + failed: 'false', + }) + + expect(notification).toEqual({ + title: 'Transaction executed', + body: 'Safe 0x0000...0001 on Mainnet executed transaction 0x0000...0004.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification({ + ...payload, + failed: 'false', + }) + + expect(notification).toEqual({ + title: 'Transaction executed', + body: 'Safe 0x0000...0001 on chain 1 executed transaction 0x0000...0004.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('failed transactions', () => { + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification({ + ...payload, + failed: 'true', + }) + + expect(notification).toEqual({ + title: 'Transaction failed', + body: 'Safe 0x0000...0001 on Mainnet failed to execute transaction 0x0000...0004.', + link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification({ + ...payload, + failed: 'true', + }) + + expect(notification).toEqual({ + title: 'Transaction failed', + body: 'Safe 0x0000...0001 on chain 1 failed to execute transaction 0x0000...0004.', + link: 'https://app.safe.global', + }) + }) + }) + }) + + describe('should parse PENDING_MULTISIG_TRANSACTION payloads', () => { + const payload: PendingMultisigTransactionEvent = { + type: WebhookType.PENDING_MULTISIG_TRANSACTION, + chainId: '1', + address: hexZeroPad('0x1', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'New pending transaction', + body: 'Safe 0x0000...0001 on Mainnet has a new pending transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'New pending transaction', + body: 'Safe 0x0000...0001 on chain 1 has a new pending transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse INCOMING_ETHER payloads', () => { + const payload: IncomingEtherEvent = { + type: WebhookType.INCOMING_ETHER, + chainId: '137', + address: hexZeroPad('0x1', 20), + txHash: hexZeroPad('0x3', 32), + value: '1000000000000000000', + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce( + setupFetchStub({ + results: [ + { + chainName: 'Polygon', + chainId: payload.chainId, + shortName: 'matic', + nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, + } as ChainInfo, + ], + }), + ) + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Matic received', + body: 'Safe 0x0000...0001 on Polygon received 1.0 MATIC in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=matic:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Ether received', + body: 'Safe 0x0000...0001 on chain 137 received 1.0 ETH in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse OUTGOING_ETHER payloads', () => { + const payload: OutgoingEtherEvent = { + type: WebhookType.OUTGOING_ETHER, + chainId: '137', + address: hexZeroPad('0x1', 20), + txHash: hexZeroPad('0x3', 32), + value: '1000000000000000000', + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce( + setupFetchStub({ + results: [ + { + chainName: 'Polygon', + chainId: payload.chainId, + shortName: 'matic', + nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, + } as ChainInfo, + ], + }), + ) + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Matic sent', + body: 'Safe 0x0000...0001 on Polygon sent 1.0 MATIC in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=matic:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Ether sent', + body: 'Safe 0x0000...0001 on chain 137 sent 1.0 ETH in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse INCOMING_TOKEN payloads', () => { + const payload: IncomingTokenEvent = { + type: WebhookType.INCOMING_TOKEN, + chainId: '1', + address: hexZeroPad('0x1', 20), + tokenAddress: hexZeroPad('0x2', 20), + txHash: hexZeroPad('0x3', 32), + } + + const erc20Payload: IncomingTokenEvent = { + ...payload, + value: '1000000000000000000', + } + + it('with chain and token info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Fake received', + body: 'Safe 0x0000...0001 on Mainnet received some FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Fake received', + body: 'Safe 0x0000...0001 on Mainnet received 1.0 FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Fake received', + body: 'Safe 0x0000...0001 on chain 1 received some FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Fake received', + body: 'Safe 0x0000...0001 on chain 1 received 1.0 FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + + it('without token info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce(() => Promise.reject()) // tokens + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Token received', + body: 'Safe 0x0000...0001 on Mainnet received some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce(() => Promise.reject()) // tokens + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Token received', + body: 'Safe 0x0000...0001 on Mainnet received some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain and balance info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce(() => Promise.reject()) // tokens + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Token received', + body: 'Safe 0x0000...0001 on chain 1 received some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce(() => Promise.reject()) // tokens + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Token received', + body: 'Safe 0x0000...0001 on chain 1 received some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse OUTGOING_TOKEN payloads', () => { + const payload: OutgoingTokenEvent = { + type: WebhookType.OUTGOING_TOKEN, + chainId: '1', + address: hexZeroPad('0x1', 20), + tokenAddress: hexZeroPad('0x2', 20), + txHash: hexZeroPad('0x3', 32), + } + + const erc20Payload: OutgoingTokenEvent = { + ...payload, + value: '1000000000000000000', + } + + it('with chain and token info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Fake sent', + body: 'Safe 0x0000...0001 on Mainnet sent some FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Fake sent', + body: 'Safe 0x0000...0001 on Mainnet sent 1.0 FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('with chain and empty token info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [], // Transaction sent all of the tokens + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Token sent', + body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Fake sent', + body: 'Safe 0x0000...0001 on Mainnet sent 1.0 FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Fake sent', + body: 'Safe 0x0000...0001 on chain 1 sent some FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce( + setupFetchStub({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + } as TokenInfo, + }, + ], + }), + ) + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Fake sent', + body: 'Safe 0x0000...0001 on chain 1 sent 1.0 FAKE in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + it('without token info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce(() => Promise.reject()) // tokens + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Token sent', + body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], + }), + ) + .mockImplementationOnce(() => Promise.reject()) // tokens + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Token sent', + body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain and balance info', async () => { + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce(() => Promise.reject()) // tokens + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Token sent', + body: 'Safe 0x0000...0001 on chain 1 sent some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + + global.fetch = jest + .fn() + .mockImplementationOnce(() => Promise.reject()) // chains + .mockImplementationOnce(() => Promise.reject()) // tokens + + const erc20Notification = await _parseWebhookNotification(erc20Payload) + + expect(erc20Notification).toEqual({ + title: 'Token sent', + body: 'Safe 0x0000...0001 on chain 1 sent some tokens in transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse MODULE_TRANSACTION payloads', () => { + const payload: ModuleTransactionEvent = { + type: WebhookType.MODULE_TRANSACTION, + chainId: '1', + address: hexZeroPad('0x1', 20), + module: hexZeroPad('0x2', 20), + txHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Module transaction', + body: 'Safe 0x0000...0001 on Mainnet executed a module transaction 0x0000...0003 from module 0x0000...0002.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Module transaction', + body: 'Safe 0x0000...0001 on chain 1 executed a module transaction 0x0000...0003 from module 0x0000...0002.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse CONFIRMATION_REQUEST payloads', () => { + const payload: ConfirmationRequestEvent = { + type: WebhookType.CONFIRMATION_REQUEST, + chainId: '1', + address: hexZeroPad('0x1', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Confirmation request', + body: 'Safe 0x0000...0001 on Mainnet has a new confirmation request for transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toEqual({ + title: 'Confirmation request', + body: 'Safe 0x0000...0001 on chain 1 has a new confirmation request for transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should not parse SAFE_CREATED payloads', () => { + const payload: SafeCreatedEvent = { + type: WebhookType.SAFE_CREATED, + chainId: '1', + address: hexZeroPad('0x1', 20), + txHash: hexZeroPad('0x3', 32), + blockNumber: '1', + } + it('with chain info', async () => { + global.fetch = jest.fn().mockImplementation( + setupFetchStub({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], + }), + ) + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toBe(undefined) + }) + + it('without chain info', async () => { + global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseWebhookNotification(payload) + + expect(notification).toBe(undefined) + }) + }) +}) diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts new file mode 100644 index 0000000000..225f1549ad --- /dev/null +++ b/src/services/firebase/index.ts @@ -0,0 +1,220 @@ +import { formatUnits } from 'ethers/lib/utils' +import type { MessagePayload } from 'firebase/messaging/sw' +import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' + +import { shortenAddress } from '@/utils/formatters' +import { AppRoutes } from '@/config/routes' +import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' +import type { WebhookEvent } from '@/services/firebase/webhooks' + +// XHR is not supported in service workers so we can't use the SDK +const BASE_URL = 'https://safe-client.safe.global' + +const getChains = async (): Promise => { + const ENDPOINT = `${BASE_URL}/v1/chains` + + const response = await fetch(ENDPOINT) + + if (response.ok) { + return response.json() + } +} + +const getBalances = async (chainId: string, safeAddress: string): Promise => { + const DEFAULT_CURRENCY = 'USD' + const ENDPOINT = `${BASE_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` + + const response = await fetch(ENDPOINT) + + if (response.ok) { + return response.json() + } +} + +const getLink = (path: string, address: string, chain?: ChainInfo) => { + const APP_URL = 'https://app.safe.global' + + if (!chain) { + return APP_URL + } + + return `${APP_URL}${path}?safe=${chain.shortName}:${address}` +} + +export const _parseWebhookNotification = async ( + data: WebhookEvent, +): Promise<{ title: string; body: string; link: string } | undefined> => { + const { type, chainId, address } = data + + let chains: Array | undefined + + try { + const response = await getChains() + chains = response?.results + } catch {} + + const chain = chains?.find((chain) => chain.chainId === chainId) + const chainName = chain?.chainName ?? `chain ${chainId}` + + const shortSafeAddress = shortenAddress(address) + + const historyLink = getLink(AppRoutes.transactions.history, address, chain) + const queueLink = getLink(AppRoutes.transactions.queue, address, chain) + + if (type === WebhookType.NEW_CONFIRMATION) { + const { owner, safeTxHash } = data + + return { + title: `New confirmation`, + body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation from ${shortenAddress( + owner, + )} on transaction ${shortenAddress(safeTxHash)}.`, + link: queueLink, + } + } + + if (type === WebhookType.EXECUTED_MULTISIG_TRANSACTION) { + const { failed, txHash } = data + + const shortTxHash = shortenAddress(txHash) + + if (failed === 'true') { + return { + title: `Transaction failed`, + body: `Safe ${shortSafeAddress} on ${chainName} failed to execute transaction ${shortTxHash}.`, + link: queueLink, + } + } else { + return { + title: `Transaction executed`, + body: `Safe ${shortSafeAddress} on ${chainName} executed transaction ${shortTxHash}.`, + link: historyLink, + } + } + } + + if (type === WebhookType.PENDING_MULTISIG_TRANSACTION) { + const { safeTxHash } = data + + return { + title: `New pending transaction`, + body: `Safe ${shortSafeAddress} on ${chainName} has a new pending transaction ${shortenAddress(safeTxHash)}.`, + link: queueLink, + } + } + + if (type === WebhookType.INCOMING_ETHER || type === WebhookType.OUTGOING_ETHER) { + const { txHash, value } = data + + const currencySymbol = chain?.nativeCurrency?.symbol ?? 'ETH' + const currencyValue = formatUnits(value, chain?.nativeCurrency?.decimals).toString() + const currencyName = chain?.nativeCurrency?.name ?? 'Ether' + + const shortTxHash = shortenAddress(txHash) + + if (type === WebhookType.INCOMING_ETHER) { + return { + title: `${currencyName} received`, + body: `Safe ${shortSafeAddress} on ${chainName} received ${currencyValue} ${currencySymbol} in transaction ${shortTxHash}.`, + link: historyLink, + } + } + + if (type === WebhookType.OUTGOING_ETHER) { + return { + title: `${currencyName} sent`, + body: `Safe ${shortSafeAddress} on ${chainName} sent ${currencyValue} ${currencySymbol} in transaction ${shortTxHash}.`, + link: historyLink, + } + } + } + + if (type === WebhookType.INCOMING_TOKEN || type === WebhookType.OUTGOING_TOKEN) { + const { tokenAddress, txHash, value } = data + + let balances: SafeBalanceResponse | undefined + + try { + balances = await getBalances(chainId, address) + } catch {} + + const tokenInfo = balances?.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo + + const tokenSymbol = tokenInfo?.symbol ?? 'tokens' + const tokenValue = value && tokenInfo ? formatUnits(value, tokenInfo.decimals).toString() : 'some' + const tokenName = tokenInfo?.name ?? 'Token' + + const shortTxHash = shortenAddress(txHash) + + if (type === WebhookType.INCOMING_TOKEN) { + return { + title: `${tokenName} received`, + body: `Safe ${shortSafeAddress} on ${chainName} received ${tokenValue} ${tokenSymbol} in transaction ${shortTxHash}.`, + link: historyLink, + } + } + + if (type === WebhookType.OUTGOING_TOKEN) { + return { + title: `${tokenName} sent`, + body: `Safe ${shortSafeAddress} on ${chainName} sent ${tokenValue} ${tokenSymbol} in transaction ${shortTxHash}.`, + link: historyLink, + } + } + } + + if (type === WebhookType.MODULE_TRANSACTION) { + const { module, txHash } = data + + return { + title: `Module transaction`, + body: `Safe ${shortSafeAddress} on ${chainName} executed a module transaction ${shortenAddress( + txHash, + )} from module ${shortenAddress(module)}.`, + link: historyLink, + } + } + + if (type === WebhookType.CONFIRMATION_REQUEST) { + const { safeTxHash } = data + + return { + title: `Confirmation request`, + body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation request for transaction ${shortenAddress( + safeTxHash, + )}.`, + link: queueLink, + } + } + + if (type === WebhookType.SAFE_CREATED) { + // Notifications are subscribed to per Safe so we would only show this notification + // if the user was subscribed to a pre-determined address + } +} + +export const parseFirebaseNotification = async ( + payload: MessagePayload, +): Promise<({ title: string; link?: string } & NotificationOptions) | undefined> => { + // Transaction Service-dispatched notification + if (isWebhookEvent(payload.data)) { + const webhookNotification = await _parseWebhookNotification(payload.data) + + if (webhookNotification) { + return { + title: webhookNotification.title, + body: webhookNotification.body, + link: webhookNotification.link, + } + } + } + + // Firebase-dispatched notification + if (payload.notification) { + return { + title: payload.notification.title || '', + body: payload.notification.body, + image: payload.notification.image, + } + } +} diff --git a/src/services/firebase/webhooks.ts b/src/services/firebase/webhooks.ts new file mode 100644 index 0000000000..693e525af6 --- /dev/null +++ b/src/services/firebase/webhooks.ts @@ -0,0 +1,111 @@ +import type { MessagePayload } from 'firebase/messaging' + +export const isWebhookEvent = (data: MessagePayload['data']): data is WebhookEvent => { + return Object.values(WebhookType).some((type) => type === data?.type) +} + +export enum WebhookType { + NEW_CONFIRMATION = 'NEW_CONFIRMATION', + EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', + PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', + INCOMING_ETHER = 'INCOMING_ETHER', + OUTGOING_ETHER = 'OUTGOING_ETHER', + INCOMING_TOKEN = 'INCOMING_TOKEN', + OUTGOING_TOKEN = 'OUTGOING_TOKEN', + MODULE_TRANSACTION = 'MODULE_TRANSACTION', + CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook + SAFE_CREATED = 'SAFE_CREATED', +} + +export type NewConfirmationEvent = { + type: WebhookType.NEW_CONFIRMATION + chainId: string + address: string + owner: string + safeTxHash: string +} + +export type ExecutedMultisigTransactionEvent = { + type: WebhookType.EXECUTED_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string + failed: 'true' | 'false' + txHash: string +} + +export type PendingMultisigTransactionEvent = { + type: WebhookType.PENDING_MULTISIG_TRANSACTION + chainId: string + address: string + safeTxHash: string +} + +export type IncomingEtherEvent = { + type: WebhookType.INCOMING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +export type OutgoingEtherEvent = { + type: WebhookType.OUTGOING_ETHER + chainId: string + address: string + txHash: string + value: string +} + +export type IncomingTokenEvent = { + type: WebhookType.INCOMING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +export type OutgoingTokenEvent = { + type: WebhookType.OUTGOING_TOKEN + chainId: string + address: string + tokenAddress: string + txHash: string + value?: string // If ERC-20 token +} + +export type ModuleTransactionEvent = { + type: WebhookType.MODULE_TRANSACTION + chainId: string + address: string + module: string + txHash: string +} + +export type ConfirmationRequestEvent = { + type: WebhookType.CONFIRMATION_REQUEST + chainId: string + address: string + safeTxHash: string +} + +export type SafeCreatedEvent = { + type: WebhookType.SAFE_CREATED + chainId: string + address: string + txHash: string + blockNumber: string +} + +export type WebhookEvent = + | NewConfirmationEvent + | ExecutedMultisigTransactionEvent + | PendingMultisigTransactionEvent + | IncomingEtherEvent + | OutgoingEtherEvent + | IncomingTokenEvent + | OutgoingTokenEvent + | ModuleTransactionEvent + | ConfirmationRequestEvent + | SafeCreatedEvent From 771ba433d30980956b9b94f2d9cf7b426a362cde Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 10 Aug 2023 16:35:55 +0200 Subject: [PATCH 08/62] fix: improve UI + add partial tests --- .../settings/Notifications/index.test.tsx | 378 ++++++++++++++++++ .../settings/Notifications/index.tsx | 315 +++++++++------ .../sidebar/SidebarNavigation/config.tsx | 4 - src/hooks/useFirebaseNotifications.ts | 1 - 4 files changed, 577 insertions(+), 121 deletions(-) create mode 100644 src/components/settings/Notifications/index.test.tsx diff --git a/src/components/settings/Notifications/index.test.tsx b/src/components/settings/Notifications/index.test.tsx new file mode 100644 index 0000000000..d940a64da9 --- /dev/null +++ b/src/components/settings/Notifications/index.test.tsx @@ -0,0 +1,378 @@ +import { hexZeroPad } from 'ethers/lib/utils' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' + +import * as notifications from '.' +import packageJson from '../../../../package.json' +import { Web3Provider } from '@ethersproject/providers' + +const getRegisterDeviceDto = ( + safeRegistrations: notifications.RegisterDeviceDto['safeRegistrations'], +): notifications.RegisterDeviceDto => { + return { + uuid: 'uuid', + cloudMessagingToken: 'token', + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: notifications.DeviceType.WEB, + version: packageJson.version, + timestamp: '69420', + safeRegistrations, + } +} + +const mockProvider = new Web3Provider(jest.fn()) + +describe('Notifications', () => { + beforeEach(() => { + jest.clearAllMocks() + global.fetch = jest.fn() + }) + + describe('requestNotificationPermission', () => { + let requestPermissionMock = jest.fn() + + beforeEach(() => { + globalThis.Notification = { + requestPermission: requestPermissionMock, + permission: 'default', + } as unknown as jest.Mocked + }) + + it('should return true and not request permission again if already granted', async () => { + globalThis.Notification = { + requestPermission: requestPermissionMock, + permission: 'granted', + } as unknown as jest.Mocked + + const result = await notifications._requestNotificationPermission() + + expect(requestPermissionMock).not.toHaveBeenCalled() + expect(result).toBe(true) + }) + + it('should return false if permission is denied', async () => { + requestPermissionMock.mockResolvedValue('denied') + + const result = await notifications._requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(false) + }) + + it('should return false if permission request throw', async () => { + requestPermissionMock.mockImplementation(Promise.reject) + + const result = await notifications._requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(false) + }) + + it('should return true if permission are granted', async () => { + requestPermissionMock.mockResolvedValue('granted') + + const result = await notifications._requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(true) + }) + }) + + describe('createRegisterSafePayload', () => { + it('should return the current registrations if the safe is already registered', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ]) + + const payload = await notifications._createRegisterSafePayload( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(payload).toBe(currentRegistration) + }) + + describe('should return a registration payload if the safe is not already registered', () => { + it.todo('should just be the current Safe if none is registered on the current chain') + + it.todo('should append the Safe if one is already registered on the chain') + }) + }) + + describe('registerSafe', () => { + it.skip('should return undefined if no registration exists and the registration failed', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + jest + .spyOn(notifications, '_createRegisterSafePayload') + .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const registration = await notifications._registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(registration).toBe(undefined) + }) + + it.skip('should return the registration payload if one exists and the registration failed', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + jest + .spyOn(notifications, '_createRegisterSafePayload') + .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [hexZeroPad('0x2', 20)], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ]) + + const registration = await notifications._registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(registration).toBe(currentRegistration) + }) + + it.skip('should return undefined if no registration exists and the registration threw', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + jest + .spyOn(notifications, '_createRegisterSafePayload') + .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) + + const registration = await notifications._registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(registration).toBe(undefined) + }) + + it.skip('should return the registration payload if one exists and the registration threw', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + jest.spyOn(notifications, '_createRegisterSafePayload').mockImplementation(() => { + return Promise.resolve(registrationPayload) + }) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.reject() + }) + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [hexZeroPad('0x2', 20)], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ]) + + const registration = await notifications._registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(registration).toBe(currentRegistration) + }) + + it.skip('should return the registration payload if the registration succeeded', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + jest + .spyOn(notifications, '_createRegisterSafePayload') + .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 200, + ok: true, + }) + }) + + const registration = await notifications._registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + expect(registration).toBe(registrationPayload) + }) + }) + + describe('unregisterSafe', () => { + it('should return the current registration if the unregistration was unsuccessful', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + const updatedRegistration = await notifications._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(updatedRegistration).toEqual(currentRegistration) + }) + + it('should return the current registration if the unregistration threw', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.reject() + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + ]) + + const updatedRegistration = await notifications._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(updatedRegistration).toEqual(currentRegistration) + }) + + it('should return the updated registration if the registration succeeded', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 200, + ok: true, + }) + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration = getRegisterDeviceDto([ + { + chainId, + safes: [safeAddress, hexZeroPad('0x2', 20)], + signatures: [hexZeroPad('0x5AFE', 65)], + }, + { + chainId: '5', + safes: [safeAddress], // Same address Safe on a different chain + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ]) + + const updatedRegistration = await notifications._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(updatedRegistration).toEqual( + getRegisterDeviceDto([ + { + chainId, + safes: [hexZeroPad('0x2', 20)], + signatures: [], + }, + currentRegistration.safeRegistrations[1], + ]), + ) + }) + }) +}) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 817342d215..234ca1d844 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -2,22 +2,48 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { Grid, Paper, Typography, Button } from '@mui/material' import { getToken, getMessaging } from 'firebase/messaging' import type { ReactElement } from 'react' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { Web3Provider } from '@ethersproject/providers' import packageJson from '../../../../package.json' import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { useCurrentChain } from '@/hooks/useChains' -import EthHashInfo from '@/components/common/EthHashInfo' +import CheckWallet from '@/components/common/CheckWallet' -const NOTIFICATIONS_LS_REGISTRATION_KEY = 'firebaseCloudMessaging' +// TODO: If we also want to display this on the "general" settings we will need to: +// - adjust the layout to list subscribed Safes with unregister buttons +// - add device removal route to gateway if we want it (it exists on Transaction Service) +// - update the below code accordingly -const enum DeviceType { +export const _requestNotificationPermission = async (): Promise => { + if (Notification.permission === 'granted') { + return true + } + + let permission: NotificationPermission | undefined + + try { + permission = await Notification.requestPermission() + } catch (e) { + console.error('Error requesting notification permission', e) + } + + const isGranted = permission === 'granted' + + if (!isGranted) { + alert('You must allow notifications to register your device.') + } + + return isGranted +} + +export const enum DeviceType { WEB = 'WEB', } -type RegisterDeviceDto = { +export type RegisterDeviceDto = { uuid: string cloudMessagingToken: string buildNumber: string @@ -32,138 +58,195 @@ type RegisterDeviceDto = { }> } -const getFirebaseToken = async () => { +export const _createRegisterSafePayload = async ( + safe: SafeInfo, + web3: Web3Provider, + currentRegistration?: RegisterDeviceDto, +): Promise => { + const MESSAGE_PREFIX = 'gnosis-safe' + + const currentChainSafeRegistrations = currentRegistration?.safeRegistrations.find( + (registration) => registration.chainId === safe.chainId, + )?.safes + + const safeAddress = safe.address.value + + // Safe is already registered + if (currentChainSafeRegistrations?.includes(safeAddress)) { + return currentRegistration + } + const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) - // Get token + // Get Firebase token const messaging = getMessaging() const token = await getToken(messaging, { vapidKey: FIREBASE_VAPID_KEY, serviceWorkerRegistration: swRegistration, }) - console.log({ token }) + // If uuid is not provided a new device will be created. + // If a uuid for an existing Safe is provided the FirebaseDevice will be updated with all the new data provided. + // Safes provided on the request are always added and never removed/replaced + // Signature must sign `keccack256('gnosis-safe{timestamp-epoch}{uuid}{cloud_messaging_token}{safes_sorted}': + // - `{timestamp-epoch}` must be an integer (no milliseconds) + // - `{safes_sorted}` must be checksummed safe addresses sorted and joined with no spaces + + // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 + + const timestamp = Math.floor(new Date().getTime() / 1000).toString() + const uuid = currentRegistration?.uuid ?? self.crypto.randomUUID() + + const safesToRegister = currentChainSafeRegistrations + ? [...currentChainSafeRegistrations, safeAddress] + : [safeAddress] + + const message = MESSAGE_PREFIX + timestamp + uuid + token + safesToRegister.join('') + const hashedMessage = keccak256(toUtf8Bytes(message)) + + const signature = await web3.getSigner().signMessage(hashedMessage) + + return { + uuid, + cloudMessagingToken: token, + buildNumber: '0', // Required value, but does not exist on web + bundle: location.origin, + deviceType: DeviceType.WEB, + version: packageJson.version, + timestamp, + safeRegistrations: [ + { + chainId: safe.chainId, + safes: safesToRegister, + signatures: [signature], + }, + ], + } +} + +export const _registerSafe = async ( + safe: SafeInfo, + web3: Web3Provider, + currentRegistration?: RegisterDeviceDto, +): Promise => { + const SAFE_REGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/register/notifications` + + let didRegister = false + + let payload: RegisterDeviceDto | undefined + + try { + payload = await _createRegisterSafePayload(safe, web3, currentRegistration) + + const response = await fetch(SAFE_REGISTRATION_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + // Gateway will return 200 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 + didRegister = response.ok && response.status === 200 + } catch (e) { + console.error('Error registering Safe', e) + } + + if (!didRegister) { + alert('Unable to register Safe.') + return currentRegistration + } + + return payload +} + +export const _unregisterSafe = async ( + safe: SafeInfo, + currentRegistration: RegisterDeviceDto, +): Promise => { + const SAFE_UNREGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/chains/${safe.chainId}/notifications/devices/${currentRegistration.uuid}/safes/${safe.address.value}` + + let didUnregister = false + + try { + const response = await fetch(SAFE_UNREGISTRATION_ENDPOINT, { + method: 'DELETE', + }) - return token + didUnregister = response.ok && response.status === 200 + } catch (e) { + console.error('Error unregistering Safe', e) + } + + if (!didUnregister) { + alert('Unable to unregister Safe.') + return currentRegistration + } + + // Remove deleted Safe from registration and clear signatures + const updatedSafeRegistrations = currentRegistration.safeRegistrations.map((registration) => { + if (registration.chainId !== safe.chainId) { + return registration + } + + const updatedSafes = registration.safes.filter((safeAddress) => safeAddress !== safe.address.value) + + return { + ...registration, + safes: updatedSafes, + signatures: [], + } + }) + + return { + ...currentRegistration, + safeRegistrations: updatedSafeRegistrations, + } } +const FIREBASE_LS_KEY = 'firebase' + export const Notifications = (): ReactElement => { const web3 = useWeb3() const { safe } = useSafeInfo() - const chain = useCurrentChain() - const [firebase, setFirebase] = useLocalStorage(NOTIFICATIONS_LS_REGISTRATION_KEY) - const handleRegister = async () => { - const DEVICE_REGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/register/notifications` + const [currentRegistration, setCurrentRegistration] = useLocalStorage(FIREBASE_LS_KEY) - const MESSAGE_PREFIX = 'gnosis-safe' + const isCurrentSafeRegistered = currentRegistration?.safeRegistrations?.some((registration) => { + return registration.safes.includes(safe.address.value) + }) + const handleRegister = async () => { if (!web3) { return } - // Request permission to show notifications if not already granted - if (Notification.permission !== 'granted') { - let permission: NotificationPermission | undefined + const isGranted = await _requestNotificationPermission() - try { - permission = await Notification.requestPermission() - } catch (e) { - console.error('Error requesting notification permission', e) - } - - if (permission !== 'granted') { - alert('You must allow notifications to register your device.') - return - } + if (!isGranted) { + return } - let didRegister = false - - // Create message to sign - const timestamp = Math.floor(new Date().getTime() / 1000).toString() - const uuid = firebase?.uuid ?? self.crypto.randomUUID() - const safes = [safe.address.value] - const token = await getFirebaseToken() - - const message = MESSAGE_PREFIX + timestamp + uuid + token + safes.join('') - const hashedMessage = keccak256(toUtf8Bytes(message)) - - let payload: RegisterDeviceDto | undefined - - // Register device - try { - const signature = await web3.getSigner().signMessage(hashedMessage) - - // TODO: Push to existing registrations? - payload = { - uuid, - cloudMessagingToken: token, - buildNumber: '0', // TODO: What do we add here? - bundle: '.', // TODO: What do we add here? - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp, - safeRegistrations: [ - { - chainId: safe.chainId, - safes, - signatures: [signature], - }, - ], - } - - const response = await fetch(DEVICE_REGISTRATION_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) - - // Gateway will return 200 if the device was registered successfully - didRegister = response?.ok && response?.status === 200 - } catch (e) { - console.error('Error registering device', e) - } + const registration = await _registerSafe(safe, web3, currentRegistration) - if (!didRegister) { - alert('Unable to register device.') - } else { - setFirebase(payload) + if (registration) { + setCurrentRegistration(registration) } } - // TODO: Fix unregister const handleUnregister = async () => { - if (!firebase) { + if (!currentRegistration) { return } - // TODO: Implement on CGW as this is not yet implemented there - const DEVICE_DELETE_ENDPOINT = `${chain?.transactionService}v1/notifications/devices/${firebase.uuid}` - - let didDelete = false - - try { - const response = await fetch(DEVICE_DELETE_ENDPOINT, { - method: 'DELETE', - }) + const unregisteration = await _unregisterSafe(safe, currentRegistration) - console.log({ response: await response.json() }) - - didDelete = response?.ok && response?.status === 200 - } catch (e) { - console.error('Error registering device', e) - } - - if (didDelete) { - setFirebase(undefined) + if (unregisteration) { + setCurrentRegistration(undefined) } } - const registeredSafes = firebase?.safeRegistrations.map((safeRegistration) => safeRegistration.safes).flat() - return ( @@ -175,26 +258,26 @@ export const Notifications = (): ReactElement => { - {firebase - ? 'You are currently registered to receive notifications about your Safe(s) on this device.' - : 'You can register to see notifications about your Safe(s) on this device. You will have to sign a message to verify that you are the owner of this Safe. Please note that you will need to register again if you clear your browser cache.'} + {isCurrentSafeRegistered + ? 'You are currently opt-in to receive notifications about this Safe on your device.' + : 'You can register to see notifications about this Safe on your device. To register, you will have to sign a message to verify that you are the owner of this Safe.'} +
+
+ Please note that registration is per-browser and you will need to register again if you clear your browser + cache.
- - {registeredSafes && ( -
- {registeredSafes.map((safe, i) => { - return - })} -
- )} - {firebase ? ( + {isCurrentSafeRegistered ? ( ) : ( - + + {(isOk) => ( + + )} + )}
diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index d3196aa5d6..27fb7dff1c 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -119,10 +119,6 @@ export const generalSettingsNavItems = [ label: 'Appearance', href: AppRoutes.settings.appearance, }, - { - label: 'Notifications', - href: AppRoutes.settings.notifications, - }, { label: 'Data', href: AppRoutes.settings.data, diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index ce21b78d8e..abc6e5f039 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -43,7 +43,6 @@ export const useFirebaseNotifications = (): null => { return } - // TODO: Should this be added to the privacy policy? const _app = initializeApp({ apiKey: FIREBASE_API_KEY, authDomain: FIREBASE_AUTH_DOMAIN, From bba709a46116593a0912c0af489fa24dab63ce13 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 10 Aug 2023 18:29:18 +0200 Subject: [PATCH 09/62] fix: add more test coverage --- .../settings/Notifications/index.test.tsx | 378 ------------ .../settings/Notifications/index.tsx | 200 +------ .../settings/Notifications/logic.test.ts | 563 ++++++++++++++++++ .../settings/Notifications/logic.ts | 200 +++++++ 4 files changed, 767 insertions(+), 574 deletions(-) delete mode 100644 src/components/settings/Notifications/index.test.tsx create mode 100644 src/components/settings/Notifications/logic.test.ts create mode 100644 src/components/settings/Notifications/logic.ts diff --git a/src/components/settings/Notifications/index.test.tsx b/src/components/settings/Notifications/index.test.tsx deleted file mode 100644 index d940a64da9..0000000000 --- a/src/components/settings/Notifications/index.test.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import { hexZeroPad } from 'ethers/lib/utils' -import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' - -import * as notifications from '.' -import packageJson from '../../../../package.json' -import { Web3Provider } from '@ethersproject/providers' - -const getRegisterDeviceDto = ( - safeRegistrations: notifications.RegisterDeviceDto['safeRegistrations'], -): notifications.RegisterDeviceDto => { - return { - uuid: 'uuid', - cloudMessagingToken: 'token', - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: notifications.DeviceType.WEB, - version: packageJson.version, - timestamp: '69420', - safeRegistrations, - } -} - -const mockProvider = new Web3Provider(jest.fn()) - -describe('Notifications', () => { - beforeEach(() => { - jest.clearAllMocks() - global.fetch = jest.fn() - }) - - describe('requestNotificationPermission', () => { - let requestPermissionMock = jest.fn() - - beforeEach(() => { - globalThis.Notification = { - requestPermission: requestPermissionMock, - permission: 'default', - } as unknown as jest.Mocked - }) - - it('should return true and not request permission again if already granted', async () => { - globalThis.Notification = { - requestPermission: requestPermissionMock, - permission: 'granted', - } as unknown as jest.Mocked - - const result = await notifications._requestNotificationPermission() - - expect(requestPermissionMock).not.toHaveBeenCalled() - expect(result).toBe(true) - }) - - it('should return false if permission is denied', async () => { - requestPermissionMock.mockResolvedValue('denied') - - const result = await notifications._requestNotificationPermission() - - expect(requestPermissionMock).toHaveBeenCalledTimes(1) - expect(result).toBe(false) - }) - - it('should return false if permission request throw', async () => { - requestPermissionMock.mockImplementation(Promise.reject) - - const result = await notifications._requestNotificationPermission() - - expect(requestPermissionMock).toHaveBeenCalledTimes(1) - expect(result).toBe(false) - }) - - it('should return true if permission are granted', async () => { - requestPermissionMock.mockResolvedValue('granted') - - const result = await notifications._requestNotificationPermission() - - expect(requestPermissionMock).toHaveBeenCalledTimes(1) - expect(result).toBe(true) - }) - }) - - describe('createRegisterSafePayload', () => { - it('should return the current registrations if the safe is already registered', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ]) - - const payload = await notifications._createRegisterSafePayload( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - currentRegistration, - ) - - expect(payload).toBe(currentRegistration) - }) - - describe('should return a registration payload if the safe is not already registered', () => { - it.todo('should just be the current Safe if none is registered on the current chain') - - it.todo('should append the Safe if one is already registered on the chain') - }) - }) - - describe('registerSafe', () => { - it.skip('should return undefined if no registration exists and the registration failed', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - jest - .spyOn(notifications, '_createRegisterSafePayload') - .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const registration = await notifications._registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(registration).toBe(undefined) - }) - - it.skip('should return the registration payload if one exists and the registration failed', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - jest - .spyOn(notifications, '_createRegisterSafePayload') - .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [hexZeroPad('0x2', 20)], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ]) - - const registration = await notifications._registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - currentRegistration, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(registration).toBe(currentRegistration) - }) - - it.skip('should return undefined if no registration exists and the registration threw', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - jest - .spyOn(notifications, '_createRegisterSafePayload') - .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) - - const registration = await notifications._registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(registration).toBe(undefined) - }) - - it.skip('should return the registration payload if one exists and the registration threw', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - jest.spyOn(notifications, '_createRegisterSafePayload').mockImplementation(() => { - return Promise.resolve(registrationPayload) - }) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.reject() - }) - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [hexZeroPad('0x2', 20)], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ]) - - const registration = await notifications._registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - currentRegistration, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(registration).toBe(currentRegistration) - }) - - it.skip('should return the registration payload if the registration succeeded', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - jest - .spyOn(notifications, '_createRegisterSafePayload') - .mockImplementation(jest.fn().mockResolvedValue(registrationPayload)) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 200, - ok: true, - }) - }) - - const registration = await notifications._registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - expect(registration).toBe(registrationPayload) - }) - }) - - describe('unregisterSafe', () => { - it('should return the current registration if the unregistration was unsuccessful', async () => { - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - const updatedRegistration = await notifications._unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(updatedRegistration).toEqual(currentRegistration) - }) - - it('should return the current registration if the unregistration threw', async () => { - global.fetch = jest.fn().mockImplementation(() => { - return Promise.reject() - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - ]) - - const updatedRegistration = await notifications._unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(updatedRegistration).toEqual(currentRegistration) - }) - - it('should return the updated registration if the registration succeeded', async () => { - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 200, - ok: true, - }) - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration = getRegisterDeviceDto([ - { - chainId, - safes: [safeAddress, hexZeroPad('0x2', 20)], - signatures: [hexZeroPad('0x5AFE', 65)], - }, - { - chainId: '5', - safes: [safeAddress], // Same address Safe on a different chain - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ]) - - const updatedRegistration = await notifications._unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(updatedRegistration).toEqual( - getRegisterDeviceDto([ - { - chainId, - safes: [hexZeroPad('0x2', 20)], - signatures: [], - }, - currentRegistration.safeRegistrations[1], - ]), - ) - }) - }) -}) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 234ca1d844..633e070052 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,210 +1,18 @@ -import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { Grid, Paper, Typography, Button } from '@mui/material' -import { getToken, getMessaging } from 'firebase/messaging' import type { ReactElement } from 'react' -import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' -import type { Web3Provider } from '@ethersproject/providers' -import packageJson from '../../../../package.json' -import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import CheckWallet from '@/components/common/CheckWallet' +import { requestNotificationPermission, registerSafe, _unregisterSafe } from './logic' +import type { RegisterDeviceDto } from './logic' // TODO: If we also want to display this on the "general" settings we will need to: // - adjust the layout to list subscribed Safes with unregister buttons // - add device removal route to gateway if we want it (it exists on Transaction Service) // - update the below code accordingly -export const _requestNotificationPermission = async (): Promise => { - if (Notification.permission === 'granted') { - return true - } - - let permission: NotificationPermission | undefined - - try { - permission = await Notification.requestPermission() - } catch (e) { - console.error('Error requesting notification permission', e) - } - - const isGranted = permission === 'granted' - - if (!isGranted) { - alert('You must allow notifications to register your device.') - } - - return isGranted -} - -export const enum DeviceType { - WEB = 'WEB', -} - -export type RegisterDeviceDto = { - uuid: string - cloudMessagingToken: string - buildNumber: string - bundle: string - deviceType: DeviceType - version: string - timestamp: string - safeRegistrations: Array<{ - chainId: string - safes: Array - signatures: Array - }> -} - -export const _createRegisterSafePayload = async ( - safe: SafeInfo, - web3: Web3Provider, - currentRegistration?: RegisterDeviceDto, -): Promise => { - const MESSAGE_PREFIX = 'gnosis-safe' - - const currentChainSafeRegistrations = currentRegistration?.safeRegistrations.find( - (registration) => registration.chainId === safe.chainId, - )?.safes - - const safeAddress = safe.address.value - - // Safe is already registered - if (currentChainSafeRegistrations?.includes(safeAddress)) { - return currentRegistration - } - - const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) - - // Get Firebase token - const messaging = getMessaging() - const token = await getToken(messaging, { - vapidKey: FIREBASE_VAPID_KEY, - serviceWorkerRegistration: swRegistration, - }) - - // If uuid is not provided a new device will be created. - // If a uuid for an existing Safe is provided the FirebaseDevice will be updated with all the new data provided. - // Safes provided on the request are always added and never removed/replaced - // Signature must sign `keccack256('gnosis-safe{timestamp-epoch}{uuid}{cloud_messaging_token}{safes_sorted}': - // - `{timestamp-epoch}` must be an integer (no milliseconds) - // - `{safes_sorted}` must be checksummed safe addresses sorted and joined with no spaces - - // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 - - const timestamp = Math.floor(new Date().getTime() / 1000).toString() - const uuid = currentRegistration?.uuid ?? self.crypto.randomUUID() - - const safesToRegister = currentChainSafeRegistrations - ? [...currentChainSafeRegistrations, safeAddress] - : [safeAddress] - - const message = MESSAGE_PREFIX + timestamp + uuid + token + safesToRegister.join('') - const hashedMessage = keccak256(toUtf8Bytes(message)) - - const signature = await web3.getSigner().signMessage(hashedMessage) - - return { - uuid, - cloudMessagingToken: token, - buildNumber: '0', // Required value, but does not exist on web - bundle: location.origin, - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp, - safeRegistrations: [ - { - chainId: safe.chainId, - safes: safesToRegister, - signatures: [signature], - }, - ], - } -} - -export const _registerSafe = async ( - safe: SafeInfo, - web3: Web3Provider, - currentRegistration?: RegisterDeviceDto, -): Promise => { - const SAFE_REGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/register/notifications` - - let didRegister = false - - let payload: RegisterDeviceDto | undefined - - try { - payload = await _createRegisterSafePayload(safe, web3, currentRegistration) - - const response = await fetch(SAFE_REGISTRATION_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) - - // Gateway will return 200 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 - didRegister = response.ok && response.status === 200 - } catch (e) { - console.error('Error registering Safe', e) - } - - if (!didRegister) { - alert('Unable to register Safe.') - return currentRegistration - } - - return payload -} - -export const _unregisterSafe = async ( - safe: SafeInfo, - currentRegistration: RegisterDeviceDto, -): Promise => { - const SAFE_UNREGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/chains/${safe.chainId}/notifications/devices/${currentRegistration.uuid}/safes/${safe.address.value}` - - let didUnregister = false - - try { - const response = await fetch(SAFE_UNREGISTRATION_ENDPOINT, { - method: 'DELETE', - }) - - didUnregister = response.ok && response.status === 200 - } catch (e) { - console.error('Error unregistering Safe', e) - } - - if (!didUnregister) { - alert('Unable to unregister Safe.') - return currentRegistration - } - - // Remove deleted Safe from registration and clear signatures - const updatedSafeRegistrations = currentRegistration.safeRegistrations.map((registration) => { - if (registration.chainId !== safe.chainId) { - return registration - } - - const updatedSafes = registration.safes.filter((safeAddress) => safeAddress !== safe.address.value) - - return { - ...registration, - safes: updatedSafes, - signatures: [], - } - }) - - return { - ...currentRegistration, - safeRegistrations: updatedSafeRegistrations, - } -} - const FIREBASE_LS_KEY = 'firebase' export const Notifications = (): ReactElement => { @@ -222,13 +30,13 @@ export const Notifications = (): ReactElement => { return } - const isGranted = await _requestNotificationPermission() + const isGranted = await requestNotificationPermission() if (!isGranted) { return } - const registration = await _registerSafe(safe, web3, currentRegistration) + const registration = await registerSafe(safe, web3, currentRegistration) if (registration) { setCurrentRegistration(registration) diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/Notifications/logic.test.ts new file mode 100644 index 0000000000..70f05663a5 --- /dev/null +++ b/src/components/settings/Notifications/logic.test.ts @@ -0,0 +1,563 @@ +import * as firebase from 'firebase/messaging' +import { hexZeroPad } from 'ethers/lib/utils' +import { Web3Provider } from '@ethersproject/providers' +import type { JsonRpcSigner } from '@ethersproject/providers' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' + +import * as logic from './logic' +import packageJson from '../../../../package.json' + +jest.mock('firebase/messaging') + +Object.defineProperty(globalThis, 'crypto', { + value: { + randomUUID: () => Math.random().toString(), + }, +}) + +Object.defineProperty(globalThis, 'navigator', { + value: { + serviceWorker: { + getRegistration: jest.fn(), + }, + }, +}) + +Object.defineProperty(globalThis, 'location', { + value: { + origin: 'https://app.safe.global', + }, +}) + +const mockProvider = new Web3Provider(jest.fn()) + +describe('Notifications', () => { + let alertMock = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + + global.fetch = jest.fn() + window.alert = alertMock + }) + + describe('requestNotificationPermission', () => { + let requestPermissionMock = jest.fn() + + beforeEach(() => { + globalThis.Notification = { + requestPermission: requestPermissionMock, + permission: 'default', + } as unknown as jest.Mocked + }) + + it('should return true and not request permission again if already granted', async () => { + globalThis.Notification = { + requestPermission: requestPermissionMock, + permission: 'granted', + } as unknown as jest.Mocked + + const result = await logic.requestNotificationPermission() + + expect(requestPermissionMock).not.toHaveBeenCalled() + expect(result).toBe(true) + }) + + it('should return false if permission is denied', async () => { + requestPermissionMock.mockResolvedValue('denied') + + const result = await logic.requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(false) + }) + + it('should return false if permission request throw', async () => { + requestPermissionMock.mockImplementation(Promise.reject) + + const result = await logic.requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(false) + }) + + it('should return true if permission are granted', async () => { + requestPermissionMock.mockResolvedValue('granted') + + const result = await logic.requestNotificationPermission() + + expect(requestPermissionMock).toHaveBeenCalledTimes(1) + expect(result).toBe(true) + }) + }) + + describe('createRegisterSafePayload', () => { + it('should return the current registrations if the safe is already registered', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + const payload = await logic.createRegisterSafePayload( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(payload).toStrictEqual(currentRegistration) + }) + + describe('should return a registration payload if the safe is not already registered', () => { + it('should just be the current Safe if none is registered on the current chain', async () => { + const token = crypto.randomUUID() + const signature = hexZeroPad('0xDEAD', 65) + + jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + jest.spyOn(mockProvider, 'getSigner').mockImplementation( + () => + ({ + signMessage: jest.fn().mockResolvedValue(signature), + } as unknown as JsonRpcSigner), + ) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const payload = await logic.createRegisterSafePayload( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(payload).toStrictEqual({ + uuid: expect.any(String), + cloudMessagingToken: token, + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [signature], + }, + ], + }) + }) + + it('should append the Safe if one is already registered on the chain', async () => { + const token = crypto.randomUUID() + const signature = hexZeroPad('0xDEAD', 65) + + jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + jest.spyOn(mockProvider, 'getSigner').mockImplementation( + () => + ({ + signMessage: jest.fn().mockResolvedValue(signature), + } as unknown as JsonRpcSigner), + ) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration = { + uuid: crypto.randomUUID(), + cloudMessagingToken: token, + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId, + safes: [hexZeroPad('0x2', 20)], + signatures: [signature], + }, + ], + } + + const payload = await logic.createRegisterSafePayload( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(payload?.timestamp).not.toBe(currentRegistration.timestamp) + expect(payload?.safeRegistrations[0].signatures).not.toBe(currentRegistration?.safeRegistrations[0].signatures) + + expect(payload).toStrictEqual({ + uuid: currentRegistration.uuid, // Same UUID + cloudMessagingToken: expect.any(String), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId, + safes: [hexZeroPad('0x2', 20), safeAddress], + signatures: [expect.any(String)], + }, + ], + }) + }) + }) + }) + + describe('registerSafe', () => { + it('should return undefined if no registration exists and the registration failed', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + jest + .spyOn(logic, 'createRegisterSafePayload') + .mockImplementation(() => Promise.resolve({} as logic.RegisterDeviceDto)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const registration = await logic.registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + + expect(registration).toBe(undefined) + }) + + it('should return the current registration if one exists and the registration failed', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + jest + .spyOn(logic, 'createRegisterSafePayload') + .mockImplementation(() => Promise.resolve({} as logic.RegisterDeviceDto)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + const registration = await logic.registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + + expect(registration).toBe(currentRegistration) + }) + + it('should return undefined if no registration exists and the registration threw', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + jest + .spyOn(logic, 'createRegisterSafePayload') + .mockImplementation(() => Promise.resolve({} as logic.RegisterDeviceDto)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.reject() + }) + + const registration = await logic.registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + + expect(registration).toBe(undefined) + }) + + it('should return the current registration if one exists and the registration threw', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + jest + .spyOn(logic, 'createRegisterSafePayload') + .mockImplementation(() => Promise.resolve({} as logic.RegisterDeviceDto)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.reject() + }) + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + const registration = await logic.registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + + expect(registration).toBe(currentRegistration) + }) + + it('should return the registration payload if the registration succeeded', async () => { + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const registrationPayload: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + jest.spyOn(logic, 'createRegisterSafePayload').mockImplementation(() => Promise.resolve(registrationPayload)) + + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 200, + ok: true, + }) + }) + + const registration = await logic.registerSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + mockProvider, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).not.toHaveBeenCalled() + + expect(registration).not.toBe(registrationPayload) + }) + }) + + describe('unregisterSafe', () => { + it('should return the current registration if the unregistration was unsuccessful', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 69420, // Failed + ok: true, + }) + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + const updatedRegistration = await logic._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') + + expect(updatedRegistration).toEqual(currentRegistration) + }) + + it('should return the current registration if the unregistration threw', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.reject() + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + ], + } + + const updatedRegistration = await logic._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).toHaveBeenCalledTimes(1) + expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') + + expect(updatedRegistration).toEqual(currentRegistration) + }) + + it('should return the updated registration if the registration succeeded', async () => { + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + json: () => Promise.resolve({}), + status: 200, + ok: true, + }) + }) + + const safeAddress = hexZeroPad('0x1', 20) + const chainId = '1' + + const currentRegistration: logic.RegisterDeviceDto = { + uuid: crypto.randomUUID(), + cloudMessagingToken: crypto.randomUUID(), + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: crypto.randomUUID(), + safeRegistrations: [ + { + chainId, + safes: [safeAddress, hexZeroPad('0x2', 20)], + signatures: [hexZeroPad('0xDEAD', 65)], + }, + { + chainId: '5', + safes: [safeAddress], // Same address Safe on a different chain + signatures: [hexZeroPad('0xBEEF', 65)], + }, + ], + } + + const updatedRegistration = await logic._unregisterSafe( + { chainId, address: { value: safeAddress } } as SafeInfo, + currentRegistration, + ) + + expect(global.fetch).toHaveBeenCalledTimes(1) + + expect(alertMock).not.toHaveBeenCalled() + + expect(updatedRegistration?.timestamp).not.toBe(currentRegistration.timestamp) + + expect(updatedRegistration).toEqual({ + uuid: currentRegistration.uuid, // Same UUID + cloudMessagingToken: currentRegistration.cloudMessagingToken, // Same token + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: logic.DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId, + safes: [hexZeroPad('0x2', 20)], + signatures: [], + }, + { + chainId: '5', + safes: [safeAddress], // Same address Safe on a different chain + signatures: [hexZeroPad('0xBEEF', 65)], + }, + ], + }) + }) + }) +}) diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts new file mode 100644 index 0000000000..e5a59bd6e9 --- /dev/null +++ b/src/components/settings/Notifications/logic.ts @@ -0,0 +1,200 @@ +import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' +import { getToken, getMessaging } from 'firebase/messaging' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { Web3Provider } from '@ethersproject/providers' + +import packageJson from '../../../../package.json' +import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY, GATEWAY_URL_STAGING } from '@/config/constants' + +export const requestNotificationPermission = async (): Promise => { + if (Notification.permission === 'granted') { + return true + } + + let permission: NotificationPermission | undefined + + try { + permission = await Notification.requestPermission() + } catch (e) { + console.error('Error requesting notification permission', e) + } + + const isGranted = permission === 'granted' + + if (!isGranted) { + alert('You must allow notifications to register your device.') + } + + return isGranted +} + +export const enum DeviceType { + WEB = 'WEB', +} + +export type RegisterDeviceDto = { + uuid: string + cloudMessagingToken: string + buildNumber: string + bundle: string + deviceType: DeviceType + version: string + timestamp: string + safeRegistrations: Array<{ + chainId: string + safes: Array + signatures: Array + }> +} + +const getTimestampWithoutMilliseconds = () => { + return Math.floor(new Date().getTime() / 1000).toString() +} + +export const createRegisterSafePayload = async ( + safe: SafeInfo, + web3: Web3Provider, + currentRegistration?: RegisterDeviceDto, +): Promise => { + const MESSAGE_PREFIX = 'gnosis-safe' + + const currentChainSafeRegistrations = currentRegistration?.safeRegistrations.find( + (registration) => registration.chainId === safe.chainId, + )?.safes + + const safeAddress = safe.address.value + + // Safe is already registered + if (currentChainSafeRegistrations?.includes(safeAddress)) { + return currentRegistration + } + + const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) + + // Get Firebase token + const messaging = getMessaging() + const token = await getToken(messaging, { + vapidKey: FIREBASE_VAPID_KEY, + serviceWorkerRegistration: swRegistration, + }) + + // If uuid is not provided a new device will be created. + // If a uuid for an existing Safe is provided the FirebaseDevice will be updated with all the new data provided. + // Safes provided on the request are always added and never removed/replaced + // Signature must sign `keccack256('gnosis-safe{timestamp-epoch}{uuid}{cloud_messaging_token}{safes_sorted}': + // - `{timestamp-epoch}` must be an integer (no milliseconds) + // - `{safes_sorted}` must be checksummed safe addresses sorted and joined with no spaces + + // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 + + const timestamp = getTimestampWithoutMilliseconds() + const uuid = currentRegistration?.uuid ?? self.crypto.randomUUID() + + const safesToRegister = currentChainSafeRegistrations + ? [...currentChainSafeRegistrations, safeAddress] + : [safeAddress] + + const message = MESSAGE_PREFIX + timestamp + uuid + token + safesToRegister.join('') + const hashedMessage = keccak256(toUtf8Bytes(message)) + + const signature = await web3.getSigner().signMessage(hashedMessage) + + return { + uuid, + cloudMessagingToken: token, + buildNumber: '0', // Required value, but does not exist on web + bundle: location.origin, + deviceType: DeviceType.WEB, + version: packageJson.version, + timestamp, + safeRegistrations: [ + { + chainId: safe.chainId, + safes: safesToRegister, + signatures: [signature], + }, + ], + } +} + +export const registerSafe = async ( + safe: SafeInfo, + web3: Web3Provider, + currentRegistration?: RegisterDeviceDto, +): Promise => { + const SAFE_REGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/register/notifications` + + let didRegister = false + + let payload: RegisterDeviceDto | undefined + + try { + payload = await createRegisterSafePayload(safe, web3, currentRegistration) + + const response = await fetch(SAFE_REGISTRATION_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + // Gateway will return 200 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 + didRegister = response.ok && response.status === 200 + } catch (e) { + console.error('Error registering Safe', e) + } + + if (!didRegister) { + alert('Unable to register Safe') + return currentRegistration + } + + return payload +} + +export const _unregisterSafe = async ( + safe: SafeInfo, + currentRegistration: RegisterDeviceDto, +): Promise => { + const SAFE_UNREGISTRATION_ENDPOINT = `${GATEWAY_URL_STAGING}/v1/chains/${safe.chainId}/notifications/devices/${currentRegistration.uuid}/safes/${safe.address.value}` + + let didUnregister = false + + try { + const response = await fetch(SAFE_UNREGISTRATION_ENDPOINT, { + method: 'DELETE', + }) + + didUnregister = response.ok && response.status === 200 + } catch (e) { + console.error('Error unregistering Safe', e) + } + + if (!didUnregister) { + alert('Unable to unregister Safe') + return currentRegistration + } + + // Remove deleted Safe from registration and clear signatures + const updatedSafeRegistrations = currentRegistration.safeRegistrations.map((registration) => { + if (registration.chainId !== safe.chainId) { + return registration + } + + const updatedSafes = registration.safes.filter((safeAddress) => safeAddress !== safe.address.value) + + return { + ...registration, + safes: updatedSafes, + signatures: [], + } + }) + + return { + ...currentRegistration, + timestamp: getTimestampWithoutMilliseconds(), + safeRegistrations: updatedSafeRegistrations, + } +} From 94918a19357181b768b5d6dd75767a3b6da4b426 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 11 Aug 2023 09:23:00 +0200 Subject: [PATCH 10/62] fix: list Safes --- .../settings/Notifications/index.tsx | 86 +++++++++++++------ .../settings/Notifications/logic.test.ts | 6 +- .../settings/Notifications/logic.ts | 2 +- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 633e070052..e847f7bdb6 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,20 +1,26 @@ -import { Grid, Paper, Typography, Button } from '@mui/material' +import { Grid, Paper, Typography, Button, IconButton, SvgIcon, Tooltip } from '@mui/material' +import DeleteIcon from '@/public/images/common/delete.svg' +import { useCallback, useMemo } from 'react' import type { ReactElement } from 'react' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import CheckWallet from '@/components/common/CheckWallet' -import { requestNotificationPermission, registerSafe, _unregisterSafe } from './logic' -import type { RegisterDeviceDto } from './logic' +import EnhancedTable from '@/components/common/EnhancedTable' +import { requestNotificationPermission, registerSafe, unregisterSafe } from '@/components/settings/Notifications/logic' +import type { RegisterDeviceDto } from '@/components/settings/Notifications/logic' +import EthHashInfo from '@/components/common/EthHashInfo' -// TODO: If we also want to display this on the "general" settings we will need to: -// - adjust the layout to list subscribed Safes with unregister buttons -// - add device removal route to gateway if we want it (it exists on Transaction Service) -// - update the below code accordingly +import tableCss from '@/components/common/EnhancedTable/styles.module.css' const FIREBASE_LS_KEY = 'firebase' +const headCells = [ + { id: 'safe', label: 'Safe' }, + { id: 'actions', label: '', sticky: true }, +] + export const Notifications = (): ReactElement => { const web3 = useWeb3() const { safe } = useSafeInfo() @@ -25,7 +31,7 @@ export const Notifications = (): ReactElement => { return registration.safes.includes(safe.address.value) }) - const handleRegister = async () => { + const handleRegister = useCallback(async () => { if (!web3) { return } @@ -38,22 +44,52 @@ export const Notifications = (): ReactElement => { const registration = await registerSafe(safe, web3, currentRegistration) - if (registration) { - setCurrentRegistration(registration) - } - } + setCurrentRegistration(registration) + }, [currentRegistration, safe, setCurrentRegistration, web3]) - const handleUnregister = async () => { + const handleUnregister = useCallback(async () => { if (!currentRegistration) { return } - const unregisteration = await _unregisterSafe(safe, currentRegistration) - - if (unregisteration) { - setCurrentRegistration(undefined) - } - } + const registration = await unregisterSafe(safe, currentRegistration) + + setCurrentRegistration(registration) + }, [currentRegistration, safe, setCurrentRegistration]) + + const rows = useMemo(() => { + return currentRegistration?.safeRegistrations.flatMap(({ safes }) => { + return safes.map((safeAddress) => { + return { + cells: { + safe: { + rawValue: safeAddress, + content: ( + + ), + }, + actions: { + rawValue: '', + sticky: true, + content: ( +
+ + {(isOk) => ( + + + + + + )} + +
+ ), + }, + }, + } + }) + }) + }, [currentRegistration?.safeRegistrations, handleUnregister]) return ( @@ -67,18 +103,14 @@ export const Notifications = (): ReactElement => { {isCurrentSafeRegistered - ? 'You are currently opt-in to receive notifications about this Safe on your device.' - : 'You can register to see notifications about this Safe on your device. To register, you will have to sign a message to verify that you are the owner of this Safe.'} + ? 'You will receive notifications about the following Safes on your device.' + : `You can opt-in to see notifications about this Safe on your device. To do so, you have to sign a message to verify that you are an owner.`}

Please note that registration is per-browser and you will need to register again if you clear your browser cache.
- {isCurrentSafeRegistered ? ( - - ) : ( + {!isCurrentSafeRegistered && ( {(isOk) => ( + )} + + )} + + {rows && } +
- - - - {isCurrentSafeRegistered - ? 'You will receive notifications about the following Safes on your device.' - : `You can opt-in to see notifications about this Safe on your device. To do so, you have to sign a message to verify that you are an owner.`} -
-
- Please note that registration is per-browser and you will need to register again if you clear your browser - cache. -
- {!isCurrentSafeRegistered && ( - - {(isOk) => ( - - )} - - )} - - {rows && } +
+ + + + + Preferences + + + + + + {Object.values(WebhookType).map((type) => { + return ( + { + setPreferences({ + ...preferences, + [type]: checked, + }) + }} + /> + } + label={type} + /> + ) + })} + + - - +
+ ) } diff --git a/src/components/settings/Notifications/useNotificationPreferences.ts b/src/components/settings/Notifications/useNotificationPreferences.ts new file mode 100644 index 0000000000..256da21ae1 --- /dev/null +++ b/src/components/settings/Notifications/useNotificationPreferences.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { entries, setMany } from 'idb-keyval' + +import { WebhookType } from '@/services/firebase/webhooks' +import { getNotificationPreferencesStore } from '@/services/firebase' + +type NotifcationPreferences = { [key in WebhookType]: boolean } + +const getDefaultPreferences = (): NotifcationPreferences => { + return Object.values(WebhookType).reduce((acc, type) => { + acc[type] = true + return acc + }, {} as NotifcationPreferences) +} + +export const useNotificationPreferences = () => { + const [preferences, _setPreferences] = useState(getDefaultPreferences) + + const customStore = useMemo(() => { + if (typeof window !== 'undefined') { + return getNotificationPreferencesStore() + } + }, []) + + useEffect(() => { + if (!customStore) { + return + } + + entries(customStore).then((entries) => { + const _preferences = Object.fromEntries(entries) + + _setPreferences((prev) => ({ ...prev, ..._preferences })) + }) + }, [customStore]) + + const setPreferences: typeof _setPreferences = useCallback( + (value) => { + const newValue = value instanceof Function ? value(preferences) : value + + setMany(Object.entries(newValue), customStore).then(() => { + _setPreferences((prev) => ({ ...prev, ...newValue })) + }) + }, + [customStore, preferences], + ) + + return [preferences, setPreferences] as const +} diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 225f1549ad..e9225b98c7 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -1,12 +1,31 @@ import { formatUnits } from 'ethers/lib/utils' +import { get } from 'idb-keyval' import type { MessagePayload } from 'firebase/messaging/sw' import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' import { shortenAddress } from '@/utils/formatters' import { AppRoutes } from '@/config/routes' import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' +import { getCustomStore } from '@/services/indexed-db' import type { WebhookEvent } from '@/services/firebase/webhooks' +export const getNotificationPreferencesStore = () => { + const STORE_NAME = 'notification-preferences' + + return getCustomStore(STORE_NAME) +} + +export const shouldShowNotification = async (payload: MessagePayload): Promise => { + if (!isWebhookEvent(payload.data)) { + return true + } + + const store = getNotificationPreferencesStore() + const preference = await get(payload.data.type, store) + + return preference ?? true +} + // XHR is not supported in service workers so we can't use the SDK const BASE_URL = 'https://safe-client.safe.global' diff --git a/src/services/indexed-db.ts b/src/services/indexed-db.ts new file mode 100644 index 0000000000..7bf08094cd --- /dev/null +++ b/src/services/indexed-db.ts @@ -0,0 +1,7 @@ +import { createStore } from 'idb-keyval' + +export const getCustomStore = (storeName: string) => { + const DB_NAME = 'safe' + + return createStore(DB_NAME, storeName) +} diff --git a/yarn.lock b/yarn.lock index ff31e589ea..8d3d411f96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10397,6 +10397,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + idb@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" From 7a4e78895c28e090ca2584947c07c316dbed55e3 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 22 Aug 2023 13:44:36 +0200 Subject: [PATCH 13/62] fix: tests --- .../settings/Notifications/logic.test.ts | 154 ++---------------- .../settings/Notifications/logic.ts | 4 +- 2 files changed, 19 insertions(+), 139 deletions(-) diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/Notifications/logic.test.ts index 1e3c1e8399..6c056ccbde 100644 --- a/src/components/settings/Notifications/logic.test.ts +++ b/src/components/settings/Notifications/logic.test.ts @@ -2,6 +2,7 @@ import * as firebase from 'firebase/messaging' import { hexZeroPad } from 'ethers/lib/utils' import { Web3Provider } from '@ethersproject/providers' import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' import type { JsonRpcSigner } from '@ethersproject/providers' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -10,6 +11,8 @@ import packageJson from '../../../../package.json' jest.mock('firebase/messaging') +jest.mock('@safe-global/safe-gateway-typescript-sdk') + Object.defineProperty(globalThis, 'crypto', { value: { randomUUID: () => Math.random().toString(), @@ -38,7 +41,6 @@ describe('Notifications', () => { beforeEach(() => { jest.clearAllMocks() - global.fetch = jest.fn() window.alert = alertMock }) @@ -224,81 +226,7 @@ describe('Notifications', () => { }) describe('registerSafe', () => { - it('should return undefined if no registration exists and the registration failed', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - jest - .spyOn(logic, 'createRegisterSafePayload') - .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const registration = await logic.registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') - - expect(registration).toBe(undefined) - }) - - it('should return the current registration if one exists and the registration failed', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - jest - .spyOn(logic, 'createRegisterSafePayload') - .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - const registration = await logic.registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - currentRegistration, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') - - expect(registration).toBe(currentRegistration) - }) + const mockRegisterSafe = jest.spyOn(sdk, 'registerDevice') it('should return undefined if no registration exists and the registration threw', async () => { const safeAddress = hexZeroPad('0x1', 20) @@ -308,7 +236,7 @@ describe('Notifications', () => { .spyOn(logic, 'createRegisterSafePayload') .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - global.fetch = jest.fn().mockImplementation(() => { + mockRegisterSafe.mockImplementation(() => { return Promise.reject() }) @@ -317,7 +245,7 @@ describe('Notifications', () => { mockProvider, ) - expect(global.fetch).toHaveBeenCalledTimes(1) + expect(mockRegisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') @@ -333,7 +261,7 @@ describe('Notifications', () => { .spyOn(logic, 'createRegisterSafePayload') .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - global.fetch = jest.fn().mockImplementation(() => { + mockRegisterSafe.mockImplementation(() => { return Promise.reject() }) @@ -360,7 +288,7 @@ describe('Notifications', () => { currentRegistration, ) - expect(global.fetch).toHaveBeenCalledTimes(1) + expect(mockRegisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') @@ -391,12 +319,8 @@ describe('Notifications', () => { jest.spyOn(logic, 'createRegisterSafePayload').mockImplementation(() => Promise.resolve(registrationPayload)) - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 200, - ok: true, - }) + mockRegisterSafe.mockImplementation(() => { + return Promise.resolve() }) const registration = await logic.registerSafe( @@ -404,7 +328,7 @@ describe('Notifications', () => { mockProvider, ) - expect(global.fetch).toHaveBeenCalledTimes(1) + expect(mockRegisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).not.toHaveBeenCalled() @@ -413,50 +337,10 @@ describe('Notifications', () => { }) describe('unregisterSafe', () => { - it('should return the current registration if the unregistration was unsuccessful', async () => { - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 69420, // Failed - ok: true, - }) - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - const updatedRegistration = await logic.unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(global.fetch).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') - - expect(updatedRegistration).toEqual(currentRegistration) - }) + const mockUnregisterSafe = jest.spyOn(sdk, 'unregisterSafe') it('should return the current registration if the unregistration threw', async () => { - global.fetch = jest.fn().mockImplementation(() => { + mockUnregisterSafe.mockImplementation(() => { return Promise.reject() }) @@ -485,7 +369,7 @@ describe('Notifications', () => { currentRegistration, ) - expect(global.fetch).toHaveBeenCalledTimes(1) + expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') @@ -494,12 +378,8 @@ describe('Notifications', () => { }) it('should return the updated registration if the registration succeeded', async () => { - global.fetch = jest.fn().mockImplementation(() => { - return Promise.resolve({ - json: () => Promise.resolve({}), - status: 200, - ok: true, - }) + mockUnregisterSafe.mockImplementation(() => { + return Promise.resolve() }) const safeAddress = hexZeroPad('0x1', 20) @@ -532,7 +412,7 @@ describe('Notifications', () => { currentRegistration, ) - expect(global.fetch).toHaveBeenCalledTimes(1) + expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).not.toHaveBeenCalled() diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts index 7586cb0c24..e1e49e325a 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/Notifications/logic.ts @@ -123,7 +123,7 @@ export const registerSafe = async ( // @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 + didRegister = response == null } } catch (e) { console.error('Error registering Safe', e) @@ -146,7 +146,7 @@ export const unregisterSafe = async ( try { const response = await gatewayUnregisterSafe(safe.chainId, safe.address.value, currentRegistration.uuid) - didUnregister = response != null + didUnregister = response == null } catch (e) { console.error('Error unregistering Safe', e) } From ef23f7c5c9c555c879146090356b203cf23f70b2 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 22 Aug 2023 20:47:24 +0200 Subject: [PATCH 14/62] fix: implement designs --- .../Notifications/AllSafesNotifications.tsx | 214 ++++++++++++++++++ .../settings/Notifications/index.tsx | 186 +++++++-------- .../settings/Notifications/logic.test.ts | 94 ++++---- .../settings/Notifications/logic.ts | 70 +++--- .../settings/Notifications/styles.module.css | 22 ++ .../sidebar/SidebarNavigation/config.tsx | 4 + 6 files changed, 409 insertions(+), 181 deletions(-) create mode 100644 src/components/settings/Notifications/AllSafesNotifications.tsx create mode 100644 src/components/settings/Notifications/styles.module.css diff --git a/src/components/settings/Notifications/AllSafesNotifications.tsx b/src/components/settings/Notifications/AllSafesNotifications.tsx new file mode 100644 index 0000000000..e348b3bcd2 --- /dev/null +++ b/src/components/settings/Notifications/AllSafesNotifications.tsx @@ -0,0 +1,214 @@ +import { + Grid, + Paper, + Typography, + Checkbox, + Button, + Divider, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, +} from '@mui/material' +import { Fragment, useMemo, useState } from 'react' +import type { ReactElement } from 'react' + +import EthHashInfo from '@/components/common/EthHashInfo' +import { sameAddress } from '@/utils/addresses' +import useChains from '@/hooks/useChains' +import { useAppSelector } from '@/store' +import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' +import CheckWallet from '@/components/common/CheckWallet' +import type { NotificationRegistration } from './logic' + +export const AllSafesNotifications = ({ + currentRegistration, + handleRegister, +}: { + currentRegistration: NotificationRegistration | undefined + handleRegister: (safesToRegister: { [chainId: string]: Array }) => Promise +}): ReactElement | null => { + const chains = useChains() + + const totalAddedSafes = useAppSelector(selectTotalAdded) + const addedSafes = useAppSelector(selectAllAddedSafes) + + const notifiableSafes = useMemo(() => { + const registerable: { [chainId: string]: Array } = {} + + for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { + registerable[chainId] = Object.keys(addedSafesOnChain) + } + + for (const { chainId, safes } of currentRegistration?.safeRegistrations ?? []) { + registerable[chainId] = Array.from(new Set([...registerable[chainId], ...safes])) + } + + return registerable + }, [addedSafes, currentRegistration?.safeRegistrations]) + + const [safesToRegister, setSafesToRegister] = useState<{ [chainId: string]: Array }>( + currentRegistration + ? currentRegistration.safeRegistrations.reduce<{ [chainId: string]: Array }>( + (acc, { chainId, safes }) => { + acc[chainId] = safes + return acc + }, + {}, + ) + : {}, + ) + + const canRegister = Object.entries(safesToRegister).some(([chainId, safes]) => { + const chainSafeRegistration = currentRegistration?.safeRegistrations.find( + (safeRegistration) => safeRegistration.chainId === chainId, + ) + + return ( + !chainSafeRegistration || + safes.length !== chainSafeRegistration.safes.length || + safes.some((address) => !chainSafeRegistration.safes.includes(address)) + ) + }) + + const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safes]) => { + const hasChain = Object.keys(safesToRegister).includes(chainId) + const hasEverySafe = safes.every((address) => safesToRegister[chainId]?.includes(address)) + return hasChain && hasEverySafe + }) + + const onSelectAll = () => { + setSafesToRegister(() => { + if (isAllSelected) { + return [] + } + + return Object.entries(notifiableSafes).reduce((acc, [chainId, safes]) => { + return { + ...acc, + [chainId]: safes, + } + }, {}) + }) + } + + const onSubscribe = async () => { + await handleRegister(safesToRegister) + + // TODO: Handle unregistration(s) + } + + if (totalAddedSafes === 0) { + return null + } + + return ( + + + + My Safes ({totalAddedSafes}) + + + + {(isOk) => ( + + )} + + + + + `1px solid ${palette.border.light}` }}> + + + + + + + + + + + + + + {Object.entries(notifiableSafes).map(([chainId, safes], i, arr) => { + const chain = chains.configs?.find((chain) => chain.chainId === chainId) + + const isChainSelected = safes.every((address) => { + return safesToRegister[chainId]?.includes(address) + }) + + const onSelectChain = () => { + setSafesToRegister((prev) => { + if (isChainSelected) { + return { + ...prev, + [chainId]: [], + } + } + + return { + ...prev, + [chainId]: safes, + } + }) + } + + return ( + + + + + + + + + + + + + {safes.map((address) => { + const isSafeSelected = safesToRegister[chainId]?.includes(address) ?? false + + const onSelectSafe = () => { + setSafesToRegister((prev) => { + if (isSafeSelected) { + return { + ...prev, + [chainId]: prev[chainId].filter((addr) => !sameAddress(addr, address)), + } + } + + return { + ...prev, + [chainId]: [...(prev[chainId] ?? []), address], + } + }) + } + + return ( + + + + + + + + + ) + })} + + + + {i !== arr.length - 1 ? : null} + + ) + })} + + + + ) +} diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index c2f984a91b..4a700787f0 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,69 +1,45 @@ -import { - Grid, - Paper, - Typography, - Button, - IconButton, - SvgIcon, - Tooltip, - Checkbox, - FormControlLabel, - FormGroup, -} from '@mui/material' -import DeleteIcon from '@/public/images/common/delete.svg' -import { useCallback, useMemo } from 'react' +import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch } from '@mui/material' import type { ReactElement } from 'react' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import useLocalStorage from '@/services/local-storage/useLocalStorage' import CheckWallet from '@/components/common/CheckWallet' -import EnhancedTable from '@/components/common/EnhancedTable' -import { requestNotificationPermission, registerSafe, unregisterSafe } from '@/components/settings/Notifications/logic' +import { registerNotifications, unregisterSafe } from '@/components/settings/Notifications/logic' import EthHashInfo from '@/components/common/EthHashInfo' import { WebhookType } from '@/services/firebase/webhooks' import { useNotificationPreferences } from './useNotificationPreferences' +import { AllSafesNotifications } from './AllSafesNotifications' import type { NotificationRegistration } from '@/components/settings/Notifications/logic' -import tableCss from '@/components/common/EnhancedTable/styles.module.css' +import css from './styles.module.css' const FIREBASE_LS_KEY = 'firebase' -const headCells = [ - { id: 'safe', label: 'Safe' }, - { id: 'actions', label: '', sticky: true }, -] - export const Notifications = (): ReactElement => { const web3 = useWeb3() - const { safe } = useSafeInfo() + const { safe, safeLoaded } = useSafeInfo() const [preferences, setPreferences] = useNotificationPreferences() const [currentRegistration, setCurrentRegistration] = useLocalStorage( FIREBASE_LS_KEY, ) - const isCurrentSafeRegistered = currentRegistration?.safeRegistrations?.some((registration) => { - return registration.safes.includes(safe.address.value) + const currentSafeRegistration = currentRegistration?.safeRegistrations.find(({ safes }) => { + return safes.includes(safe.address.value) }) - const handleRegister = useCallback(async () => { + const handleRegister = async (safesToRegister: { [chainId: string]: Array }) => { if (!web3) { return } - const isGranted = await requestNotificationPermission() - - if (!isGranted) { - return - } - - const registration = await registerSafe(safe, web3, currentRegistration) + const registration = await registerNotifications(safesToRegister, web3, currentRegistration) setCurrentRegistration(registration) - }, [currentRegistration, safe, setCurrentRegistration, web3]) + } - const handleUnregister = useCallback(async () => { + const handleUnregister = async () => { if (!currentRegistration) { return } @@ -71,41 +47,15 @@ export const Notifications = (): ReactElement => { const registration = await unregisterSafe(safe, currentRegistration) setCurrentRegistration(registration) - }, [currentRegistration, safe, setCurrentRegistration]) - - const rows = useMemo(() => { - return currentRegistration?.safeRegistrations.flatMap(({ safes }) => { - return safes.map((safeAddress) => { - return { - cells: { - safe: { - rawValue: safeAddress, - content: ( - - ), - }, - actions: { - rawValue: '', - sticky: true, - content: ( -
- - {(isOk) => ( - - - - - - )} - -
- ), - }, - }, - } - }) - }) - }, [currentRegistration?.safeRegistrations, handleUnregister]) + } + + const handleOnChange = () => { + if (currentSafeRegistration) { + handleUnregister() + } else { + handleRegister({ [safe.chainId]: [safe.address.value] }) + } + } return ( <> @@ -117,27 +67,45 @@ export const Notifications = (): ReactElement => { - - - {isCurrentSafeRegistered - ? 'You will receive notifications about the following Safes on your device.' - : `You can opt-in to see notifications about this Safe on your device. To do so, you have to sign a message to verify that you are an owner.`} -
-
- Please note that registration is per-browser and you will need to register again if you clear your browser - cache. -
- {!isCurrentSafeRegistered && ( - - {(isOk) => ( - - )} - - )} - - {rows && } + + + + {currentSafeRegistration + ? 'You will receive notifications about this Safe Account in this browser.' + : `Subscribe to receive notifications about ${ + safeLoaded ? 'this Safe Account' : 'Safe Accounts' + } in this browser. To do so, you will have to sign a message to verify that you are an owner.`} + + + + Please note that registration is per-browser and you will need to register again if you clear your + browser cache. + + + {safeLoaded ? ( +
+ + {(isOk) => ( + + )} + + +
+ ) : ( + + )} +
@@ -151,25 +119,23 @@ export const Notifications = (): ReactElement => { - {Object.values(WebhookType).map((type) => { - return ( - { - setPreferences({ - ...preferences, - [type]: checked, - }) - }} - /> - } - label={type} - /> - ) - })} + {Object.values(WebhookType).map((type) => ( + { + setPreferences((prev) => ({ + ...prev, + [type]: checked, + })) + }} + /> + } + label={type} + /> + ))} diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/Notifications/logic.test.ts index 6c056ccbde..dd5822a440 100644 --- a/src/components/settings/Notifications/logic.test.ts +++ b/src/components/settings/Notifications/logic.test.ts @@ -94,14 +94,25 @@ describe('Notifications', () => { }) }) - describe('createRegisterSafePayload', () => { - it('should return the current registrations if the safe is already registered', async () => { + describe('createRegisterDevicePayload', () => { + it('should return the current registration if it is the same', async () => { + const token = crypto.randomUUID() + const signature = hexZeroPad('0xDEAD', 65) + + jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + jest.spyOn(mockProvider, 'getSigner').mockImplementation( + () => + ({ + signMessage: jest.fn().mockResolvedValue(signature), + } as unknown as JsonRpcSigner), + ) + const safeAddress = hexZeroPad('0x1', 20) const chainId = '1' const currentRegistration: logic.NotificationRegistration = { uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), + cloudMessagingToken: token, buildNumber: '0', bundle: 'https://app.safe.global', deviceType: DeviceType.WEB, @@ -116,17 +127,17 @@ describe('Notifications', () => { ], } - const payload = await logic.createRegisterSafePayload( - { chainId, address: { value: safeAddress } } as SafeInfo, + const payload = await logic.createRegisterDevicePayload( + { [chainId]: [safeAddress] }, mockProvider, currentRegistration, ) - expect(payload).toStrictEqual(currentRegistration) + expect(payload).toStrictEqual({ ...currentRegistration, timestamp: expect.any(String) }) }) - describe('should return a registration payload if the safe is not already registered', () => { - it('should just be the current Safe if none is registered on the current chain', async () => { + describe('should return a registration payload if the chain registration(s) is not already registered', () => { + it('if none on the same chain is registered', async () => { const token = crypto.randomUUID() const signature = hexZeroPad('0xDEAD', 65) @@ -141,10 +152,7 @@ describe('Notifications', () => { const safeAddress = hexZeroPad('0x1', 20) const chainId = '1' - const payload = await logic.createRegisterSafePayload( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) + const payload = await logic.createRegisterDevicePayload({ [chainId]: [safeAddress] }, mockProvider) expect(payload).toStrictEqual({ uuid: expect.any(String), @@ -164,7 +172,7 @@ describe('Notifications', () => { }) }) - it('should append the Safe if one is already registered on the chain', async () => { + it('if others on the same chain exists', async () => { const token = crypto.randomUUID() const signature = hexZeroPad('0xDEAD', 65) @@ -176,7 +184,7 @@ describe('Notifications', () => { } as unknown as JsonRpcSigner), ) - const safeAddress = hexZeroPad('0x1', 20) + const safeAddress = hexZeroPad('0x3', 20) const chainId = '1' const currentRegistration = { @@ -190,20 +198,25 @@ describe('Notifications', () => { safeRegistrations: [ { chainId, - safes: [hexZeroPad('0x2', 20)], + safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + signatures: [hexZeroPad('0xBEEF', 65)], + }, + { + chainId: '2', + safes: [hexZeroPad('0x4', 20)], signatures: [signature], }, ], } - const payload = await logic.createRegisterSafePayload( - { chainId, address: { value: safeAddress } } as SafeInfo, + const payload = await logic.createRegisterDevicePayload( + { [chainId]: [safeAddress] }, mockProvider, currentRegistration, ) - expect(payload?.timestamp).not.toBe(currentRegistration.timestamp) - expect(payload?.safeRegistrations[0].signatures).not.toBe(currentRegistration?.safeRegistrations[0].signatures) + expect(payload.timestamp).not.toBe(currentRegistration.timestamp) + expect(payload.safeRegistrations[0].signatures).not.toBe(currentRegistration.safeRegistrations[0].signatures) expect(payload).toStrictEqual({ uuid: currentRegistration.uuid, // Same UUID @@ -216,8 +229,13 @@ describe('Notifications', () => { safeRegistrations: [ { chainId, - safes: [hexZeroPad('0x2', 20), safeAddress], - signatures: [expect.any(String)], + safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20), safeAddress], + signatures: [signature], + }, + { + chainId: '2', + safes: [hexZeroPad('0x4', 20)], + signatures: [signature], }, ], }) @@ -225,30 +243,29 @@ describe('Notifications', () => { }) }) - describe('registerSafe', () => { + describe('registerNotifications', () => { const mockRegisterSafe = jest.spyOn(sdk, 'registerDevice') it('should return undefined if no registration exists and the registration threw', async () => { const safeAddress = hexZeroPad('0x1', 20) const chainId = '1' + jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + jest - .spyOn(logic, 'createRegisterSafePayload') + .spyOn(logic, 'createRegisterDevicePayload') .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) mockRegisterSafe.mockImplementation(() => { return Promise.reject() }) - const registration = await logic.registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) + const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) expect(mockRegisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') expect(registration).toBe(undefined) }) @@ -257,8 +274,10 @@ describe('Notifications', () => { const safeAddress = hexZeroPad('0x1', 20) const chainId = '1' + jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + jest - .spyOn(logic, 'createRegisterSafePayload') + .spyOn(logic, 'createRegisterDevicePayload') .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) mockRegisterSafe.mockImplementation(() => { @@ -282,8 +301,8 @@ describe('Notifications', () => { ], } - const registration = await logic.registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, + const registration = await logic.registerNotifications( + { [chainId]: [safeAddress] }, mockProvider, currentRegistration, ) @@ -291,12 +310,14 @@ describe('Notifications', () => { expect(mockRegisterSafe).toHaveBeenCalledTimes(1) expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe') + expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') expect(registration).toBe(currentRegistration) }) it('should return the registration payload if the registration succeeded', async () => { + jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + const safeAddress = hexZeroPad('0x1', 20) const chainId = '1' @@ -317,16 +338,13 @@ describe('Notifications', () => { ], } - jest.spyOn(logic, 'createRegisterSafePayload').mockImplementation(() => Promise.resolve(registrationPayload)) + jest.spyOn(logic, 'createRegisterDevicePayload').mockImplementation(() => Promise.resolve(registrationPayload)) mockRegisterSafe.mockImplementation(() => { return Promise.resolve() }) - const registration = await logic.registerSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - mockProvider, - ) + const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) expect(mockRegisterSafe).toHaveBeenCalledTimes(1) @@ -416,7 +434,7 @@ describe('Notifications', () => { expect(alertMock).not.toHaveBeenCalled() - expect(updatedRegistration?.timestamp).not.toBe(currentRegistration.timestamp) + expect(updatedRegistration.timestamp).not.toBe(currentRegistration.timestamp) expect(updatedRegistration).toEqual({ uuid: currentRegistration.uuid, // Same UUID diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts index e1e49e325a..5af6ba5ebe 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/Notifications/logic.ts @@ -6,8 +6,8 @@ import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' -import packageJson from '../../../../package.json' import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/config/constants' +import packageJson from '../../../../package.json' type WithRequired = T & { [P in K]-?: T[P] } @@ -40,22 +40,19 @@ const getTimestampWithoutMilliseconds = () => { return Math.floor(new Date().getTime() / 1000).toString() } -export const createRegisterSafePayload = async ( - safe: SafeInfo, +export const createRegisterDevicePayload = async ( + safesToRegister: { [chainId: string]: Array }, web3: Web3Provider, currentRegistration?: NotificationRegistration, -): Promise => { +): Promise => { const MESSAGE_PREFIX = 'gnosis-safe' - const currentChainSafeRegistrations = currentRegistration?.safeRegistrations.find( - (registration) => registration.chainId === safe.chainId, - )?.safes - - const safeAddress = safe.address.value - - // Safe is already registered - if (currentChainSafeRegistrations?.includes(safeAddress)) { - return currentRegistration + for (const { chainId, safes } of currentRegistration?.safeRegistrations ?? []) { + if (safesToRegister[chainId]) { + safesToRegister[chainId] = Array.from(new Set([...safes, ...safesToRegister[chainId]])) + } else { + safesToRegister[chainId] = safes + } } const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) @@ -79,14 +76,21 @@ export const createRegisterSafePayload = async ( const timestamp = getTimestampWithoutMilliseconds() const uuid = currentRegistration?.uuid ?? self.crypto.randomUUID() - const safesToRegister = currentChainSafeRegistrations - ? [...currentChainSafeRegistrations, safeAddress] - : [safeAddress] - - const message = MESSAGE_PREFIX + timestamp + uuid + token + safesToRegister.join('') - const hashedMessage = keccak256(toUtf8Bytes(message)) - - const signature = await web3.getSigner().signMessage(hashedMessage) + const safeRegistrations = await Promise.all( + Object.entries(safesToRegister) + .filter(([, safes]) => safes.length > 0) + .map(async ([chainId, safes]) => { + const message = MESSAGE_PREFIX + timestamp + uuid + token + safes.join('') + const hashedMessage = keccak256(toUtf8Bytes(message)) + const signature = await web3.getSigner().signMessage(hashedMessage) + + return { + chainId, + safes, + signatures: [signature], + } + }), + ) return { uuid, @@ -96,27 +100,27 @@ export const createRegisterSafePayload = async ( deviceType: DeviceType.WEB, version: packageJson.version, timestamp, - safeRegistrations: [ - { - chainId: safe.chainId, - safes: safesToRegister, - signatures: [signature], - }, - ], + safeRegistrations, } } -export const registerSafe = async ( - safe: SafeInfo, +export const registerNotifications = async ( + safesToRegister: { [chainId: string]: Array }, web3: Web3Provider, currentRegistration?: NotificationRegistration, ): Promise => { + const isGranted = await requestNotificationPermission() + + if (!isGranted) { + return currentRegistration + } + let didRegister = false let payload: NotificationRegistration | undefined try { - payload = await createRegisterSafePayload(safe, web3, currentRegistration) + payload = await createRegisterDevicePayload(safesToRegister, web3, currentRegistration) if (payload) { // Gateway will return 200 with an empty payload if the device was registered successfully @@ -126,11 +130,11 @@ export const registerSafe = async ( didRegister = response == null } } catch (e) { - console.error('Error registering Safe', e) + console.error('Error registering Safe(s)', e) } if (!didRegister) { - alert('Unable to register Safe') + alert('Unable to register Safe(s)') return currentRegistration } diff --git a/src/components/settings/Notifications/styles.module.css b/src/components/settings/Notifications/styles.module.css new file mode 100644 index 0000000000..20b056d7ca --- /dev/null +++ b/src/components/settings/Notifications/styles.module.css @@ -0,0 +1,22 @@ +.info { + border-color: var(--color-border-light); + background-color: var(--color-background-main); + padding: var(--space-2); +} + +.info :global .MuiAlert-icon { + color: var(--color-text-main); + padding: 0; +} + +.info :global .MuiAlert-message { + padding: 0; +} + +/* TODO: Move to theme */ +.switch :global .MuiSwitch-switchBase { + color: var(--color-primary-main); +} +.switch :global .MuiSwitch-track { + background-color: var(--color-primary-main) !important; +} diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 27fb7dff1c..d3196aa5d6 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -119,6 +119,10 @@ export const generalSettingsNavItems = [ label: 'Appearance', href: AppRoutes.settings.appearance, }, + { + label: 'Notifications', + href: AppRoutes.settings.notifications, + }, { label: 'Data', href: AppRoutes.settings.data, From 1ded885ca67ff03ed3cbd248e9bd35173dd8ced4 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 23 Aug 2023 09:35:38 +0200 Subject: [PATCH 15/62] feat: per-Safe preferences --- .../settings/Notifications/index.tsx | 127 +++++++++++++++--- .../useNotificationPreferences.ts | 8 +- src/services/firebase/index.ts | 17 +-- src/services/indexed-db.ts | 7 - 4 files changed, 123 insertions(+), 36 deletions(-) delete mode 100644 src/services/indexed-db.ts diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 4a700787f0..065f656983 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -109,37 +109,128 @@ export const Notifications = (): ReactElement => { - - - - - Preferences - - + {safeLoaded && ( + + + + + Preferences + + - - - {Object.values(WebhookType).map((type) => ( + + {/* TODO: Confirm order */} + { setPreferences((prev) => ({ ...prev, - [type]: checked, + [WebhookType.INCOMING_ETHER]: checked, + [WebhookType.INCOMING_TOKEN]: checked, })) }} /> } - label={type} + label="Incoming assets" /> - ))} - + + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.OUTGOING_ETHER]: checked, + [WebhookType.OUTGOING_TOKEN]: checked, + })) + }} + /> + } + label="Outgoing assets" + /> + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, + })) + }} + /> + } + label="Pending transactions" + /> + + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.CONFIRMATION_REQUEST]: checked, + })) + }} + /> + } + label="Confirmation requests" + /> + + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.NEW_CONFIRMATION]: checked, + })) + }} + /> + } + label="New confirmations" + /> + + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, + })) + }} + /> + } + label="Executed transactions" + /> + + { + setPreferences((prev) => ({ + ...prev, + [WebhookType.MODULE_TRANSACTION]: checked, + })) + }} + /> + } + label="Module transactions" + /> + + - - + + )} ) } diff --git a/src/components/settings/Notifications/useNotificationPreferences.ts b/src/components/settings/Notifications/useNotificationPreferences.ts index 256da21ae1..b584239107 100644 --- a/src/components/settings/Notifications/useNotificationPreferences.ts +++ b/src/components/settings/Notifications/useNotificationPreferences.ts @@ -3,6 +3,7 @@ import { entries, setMany } from 'idb-keyval' import { WebhookType } from '@/services/firebase/webhooks' import { getNotificationPreferencesStore } from '@/services/firebase' +import useSafeInfo from '@/hooks/useSafeInfo' type NotifcationPreferences = { [key in WebhookType]: boolean } @@ -14,13 +15,14 @@ const getDefaultPreferences = (): NotifcationPreferences => { } export const useNotificationPreferences = () => { + const { safe } = useSafeInfo() const [preferences, _setPreferences] = useState(getDefaultPreferences) const customStore = useMemo(() => { - if (typeof window !== 'undefined') { - return getNotificationPreferencesStore() + if (typeof window !== 'undefined' && safe.address.value) { + return getNotificationPreferencesStore(safe.address.value) } - }, []) + }, [safe.address.value]) useEffect(() => { if (!customStore) { diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index e9225b98c7..23409aa815 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -1,18 +1,18 @@ import { formatUnits } from 'ethers/lib/utils' -import { get } from 'idb-keyval' +import { createStore, get } from 'idb-keyval' import type { MessagePayload } from 'firebase/messaging/sw' import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' import { shortenAddress } from '@/utils/formatters' import { AppRoutes } from '@/config/routes' import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' -import { getCustomStore } from '@/services/indexed-db' import type { WebhookEvent } from '@/services/firebase/webhooks' +import { GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' -export const getNotificationPreferencesStore = () => { - const STORE_NAME = 'notification-preferences' +export const getNotificationPreferencesStore = (safeAddress: string) => { + const DB_NAME = 'notification-preferences' - return getCustomStore(STORE_NAME) + return createStore(DB_NAME, safeAddress) } export const shouldShowNotification = async (payload: MessagePayload): Promise => { @@ -20,15 +20,16 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise => { const ENDPOINT = `${BASE_URL}/v1/chains` diff --git a/src/services/indexed-db.ts b/src/services/indexed-db.ts deleted file mode 100644 index 7bf08094cd..0000000000 --- a/src/services/indexed-db.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createStore } from 'idb-keyval' - -export const getCustomStore = (storeName: string) => { - const DB_NAME = 'safe' - - return createStore(DB_NAME, storeName) -} From ea9a72a97ec23a7ea94332b4db718e79602d7b30 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 23 Aug 2023 17:46:27 +0200 Subject: [PATCH 16/62] refactor: move registrations to IndexedDB --- .../Notifications/AllSafesNotifications.tsx | 177 +++-- .../settings/Notifications/index.tsx | 222 +++--- .../settings/Notifications/logic.test.ts | 739 +++++++++--------- .../settings/Notifications/logic.ts | 173 ++-- .../Notifications/useNotificationDb.ts | 152 ++++ .../useNotificationPreferences.ts | 51 -- src/services/firebase/index.ts | 19 +- src/services/firebase/notification-db.ts | 14 + 8 files changed, 868 insertions(+), 679 deletions(-) create mode 100644 src/components/settings/Notifications/useNotificationDb.ts delete mode 100644 src/components/settings/Notifications/useNotificationPreferences.ts create mode 100644 src/services/firebase/notification-db.ts diff --git a/src/components/settings/Notifications/AllSafesNotifications.tsx b/src/components/settings/Notifications/AllSafesNotifications.tsx index e348b3bcd2..1d03ed439c 100644 --- a/src/components/settings/Notifications/AllSafesNotifications.tsx +++ b/src/components/settings/Notifications/AllSafesNotifications.tsx @@ -11,7 +11,8 @@ import { ListItemIcon, ListItemText, } from '@mui/material' -import { Fragment, useMemo, useState } from 'react' +import { unregisterDevice } from '@safe-global/safe-gateway-typescript-sdk' +import { Fragment, useEffect, useMemo, useState } from 'react' import type { ReactElement } from 'react' import EthHashInfo from '@/components/common/EthHashInfo' @@ -20,83 +21,145 @@ import useChains from '@/hooks/useChains' import { useAppSelector } from '@/store' import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' import CheckWallet from '@/components/common/CheckWallet' -import type { NotificationRegistration } from './logic' - -export const AllSafesNotifications = ({ - currentRegistration, - handleRegister, -}: { - currentRegistration: NotificationRegistration | undefined - handleRegister: (safesToRegister: { [chainId: string]: Array }) => Promise -}): ReactElement | null => { - const chains = useChains() +import { registerNotificationDevice, requestNotificationPermission, unregisterSafeNotifications } from './logic' +import { useNotificationDb } from './useNotificationDb' +export const AllSafesNotifications = (): ReactElement | null => { + const chains = useChains() const totalAddedSafes = useAppSelector(selectTotalAdded) const addedSafes = useAppSelector(selectAllAddedSafes) + const { + deviceUuid, + locallyRegisteredSafes, + registerSafeLocally, + unregisterSafeLocally, + clearLocallyRegisteredSafes, + } = useNotificationDb() + + const [selectedSafes, setSelectedSafes] = useState(() => locallyRegisteredSafes) + + // `locallyRegisteredSafes` is initially undefined until indexedDB resolves + useEffect(() => { + setSelectedSafes(locallyRegisteredSafes) + }, [locallyRegisteredSafes]) + + // Merge added Safes and locally notification-registered Safes const notifiableSafes = useMemo(() => { const registerable: { [chainId: string]: Array } = {} + // Added Safes for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { registerable[chainId] = Object.keys(addedSafesOnChain) } - for (const { chainId, safes } of currentRegistration?.safeRegistrations ?? []) { - registerable[chainId] = Array.from(new Set([...registerable[chainId], ...safes])) + // Locally registered Safes (if not already added) + for (const safeAddress of Object.keys(locallyRegisteredSafes)) { + const [chainId, address] = safeAddress.split(':') + + if (chainId && address) { + registerable[chainId] = Array.from(new Set([...registerable[chainId], address])) + } } return registerable - }, [addedSafes, currentRegistration?.safeRegistrations]) - - const [safesToRegister, setSafesToRegister] = useState<{ [chainId: string]: Array }>( - currentRegistration - ? currentRegistration.safeRegistrations.reduce<{ [chainId: string]: Array }>( - (acc, { chainId, safes }) => { - acc[chainId] = safes - return acc - }, - {}, - ) - : {}, - ) - - const canRegister = Object.entries(safesToRegister).some(([chainId, safes]) => { - const chainSafeRegistration = currentRegistration?.safeRegistrations.find( - (safeRegistration) => safeRegistration.chainId === chainId, - ) + }, [addedSafes, locallyRegisteredSafes]) - return ( - !chainSafeRegistration || - safes.length !== chainSafeRegistration.safes.length || - safes.some((address) => !chainSafeRegistration.safes.includes(address)) - ) - }) - - const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safes]) => { - const hasChain = Object.keys(safesToRegister).includes(chainId) - const hasEverySafe = safes.every((address) => safesToRegister[chainId]?.includes(address)) + const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { + const hasChain = Object.keys(selectedSafes).includes(chainId) + const hasEverySafe = safeAddresses.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) return hasChain && hasEverySafe }) const onSelectAll = () => { - setSafesToRegister(() => { + setSelectedSafes(() => { if (isAllSelected) { return [] } - return Object.entries(notifiableSafes).reduce((acc, [chainId, safes]) => { + return Object.entries(notifiableSafes).reduce((acc, [chainId, safeAddresses]) => { return { ...acc, - [chainId]: safes, + [chainId]: safeAddresses, } }, {}) }) } - const onSubscribe = async () => { - await handleRegister(safesToRegister) + const shouldRegisterSafes = Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { + return safeAddresses.some((safeAddress) => !locallyRegisteredSafes[chainId]?.includes(safeAddress)) + }) + const shouldUnregisterSafes = Object.entries(locallyRegisteredSafes).some(([chainId, safeAddresses]) => { + return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) + }) + const canSave = shouldRegisterSafes || shouldUnregisterSafes + + const onSave = async () => { + if (!canSave) { + return + } + + const isGranted = await requestNotificationPermission() + + if (!isGranted) { + return + } + + const shouldUnregisterDevice = Object.values(selectedSafes).every((safeAddresses) => safeAddresses.length === 0) + + if (shouldUnregisterDevice) { + // Device unregister is chain agnostic + await unregisterDevice('1', deviceUuid) + + clearLocallyRegisteredSafes() + return + } + + const promises = [] + const safesToRegister: { [chainId: string]: Array } = {} + + for (const [chainId, safeAddresses] of Object.entries(selectedSafes)) { + for (const safeAddress of safeAddresses) { + const shouldUnregister = locallyRegisteredSafes[chainId]?.includes(safeAddress) + + if (shouldUnregister) { + promises.push( + unregisterSafeNotifications({ + chainId, + safeAddress: safeAddress, + deviceUuid, + callback: () => unregisterSafeLocally(chainId, safeAddress), + }), + ) + } else { + if (!safesToRegister[chainId]) { + safesToRegister[chainId] = [] + } + + safesToRegister[chainId].push(safeAddress) + } + } + } + + if (Object.keys(safesToRegister).length > 0) { + const callback = () => { + Object.entries(safesToRegister).forEach(([chainId, safeAddresses]) => { + safeAddresses.forEach((safeAddress) => { + registerSafeLocally(chainId, safeAddress) + }) + }) + } + + promises.push( + registerNotificationDevice({ + safesToRegister, + deviceUuid, + callback, + }), + ) + } - // TODO: Handle unregistration(s) + Promise.all(promises) } if (totalAddedSafes === 0) { @@ -112,8 +175,8 @@ export const AllSafesNotifications = ({ {(isOk) => ( - )} @@ -134,15 +197,15 @@ export const AllSafesNotifications = ({ - {Object.entries(notifiableSafes).map(([chainId, safes], i, arr) => { + {Object.entries(notifiableSafes).map(([chainId, safeAddresses], i, arr) => { const chain = chains.configs?.find((chain) => chain.chainId === chainId) - const isChainSelected = safes.every((address) => { - return safesToRegister[chainId]?.includes(address) + const isChainSelected = safeAddresses.every((address) => { + return selectedSafes[chainId]?.includes(address) }) const onSelectChain = () => { - setSafesToRegister((prev) => { + setSelectedSafes((prev) => { if (isChainSelected) { return { ...prev, @@ -152,7 +215,7 @@ export const AllSafesNotifications = ({ return { ...prev, - [chainId]: safes, + [chainId]: safeAddresses, } }) } @@ -170,11 +233,11 @@ export const AllSafesNotifications = ({ - {safes.map((address) => { - const isSafeSelected = safesToRegister[chainId]?.includes(address) ?? false + {safeAddresses.map((address) => { + const isSafeSelected = selectedSafes[chainId]?.includes(address) ?? false const onSelectSafe = () => { - setSafesToRegister((prev) => { + setSelectedSafes((prev) => { if (isSafeSelected) { return { ...prev, diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 065f656983..016f56a06d 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,59 +1,45 @@ -import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch } from '@mui/material' +import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch, Divider } from '@mui/material' import type { ReactElement } from 'react' -import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' -import useLocalStorage from '@/services/local-storage/useLocalStorage' import CheckWallet from '@/components/common/CheckWallet' -import { registerNotifications, unregisterSafe } from '@/components/settings/Notifications/logic' import EthHashInfo from '@/components/common/EthHashInfo' import { WebhookType } from '@/services/firebase/webhooks' -import { useNotificationPreferences } from './useNotificationPreferences' +import { useNotificationDb } from './useNotificationDb' import { AllSafesNotifications } from './AllSafesNotifications' -import type { NotificationRegistration } from '@/components/settings/Notifications/logic' +import useIsSafeOwner from '@/hooks/useIsSafeOwner' +import { registerNotificationDevice, unregisterSafeNotifications } from './logic' +import { useWeb3 } from '@/hooks/wallets/web3' import css from './styles.module.css' -const FIREBASE_LS_KEY = 'firebase' - export const Notifications = (): ReactElement => { const web3 = useWeb3() const { safe, safeLoaded } = useSafeInfo() - const [preferences, setPreferences] = useNotificationPreferences() - - const [currentRegistration, setCurrentRegistration] = useLocalStorage( - FIREBASE_LS_KEY, - ) - - const currentSafeRegistration = currentRegistration?.safeRegistrations.find(({ safes }) => { - return safes.includes(safe.address.value) - }) - - const handleRegister = async (safesToRegister: { [chainId: string]: Array }) => { - if (!web3) { - return - } - - const registration = await registerNotifications(safesToRegister, web3, currentRegistration) - - setCurrentRegistration(registration) - } - - const handleUnregister = async () => { - if (!currentRegistration) { - return - } - - const registration = await unregisterSafe(safe, currentRegistration) - - setCurrentRegistration(registration) - } + const isOwner = useIsSafeOwner() + const { + deviceUuid, + isSafeRegistered, + notificationPreferences, + setNotificationPreferences, + registerSafeLocally, + unregisterSafeLocally, + } = useNotificationDb() const handleOnChange = () => { - if (currentSafeRegistration) { - handleUnregister() + if (isSafeRegistered) { + unregisterSafeNotifications({ + deviceUuid, + chainId: safe.chainId, + safeAddress: safe.address.value, + callback: () => unregisterSafeLocally(safe.chainId, safe.address.value), + }) } else { - handleRegister({ [safe.chainId]: [safe.address.value] }) + registerNotificationDevice({ + deviceUuid, + safesToRegister: { [safe.chainId]: [safe.address.value] }, + callback: () => registerSafeLocally(safe.chainId, safe.address.value), + }) } } @@ -67,14 +53,14 @@ export const Notifications = (): ReactElement => { - + - {currentSafeRegistration + {isSafeRegistered ? 'You will receive notifications about this Safe Account in this browser.' : `Subscribe to receive notifications about ${ safeLoaded ? 'this Safe Account' : 'Safe Accounts' - } in this browser. To do so, you will have to sign a message to verify that you are an owner.`} + } in this browser.`} @@ -83,27 +69,36 @@ export const Notifications = (): ReactElement => { {safeLoaded ? ( -
- - {(isOk) => ( - - )} - - -
+ <> + + +
+ + + {(isOk) => ( + + } + label={!!isSafeRegistered ? 'On' : 'Off'} + /> + )} + +
+ ) : ( - + )}
@@ -119,19 +114,22 @@ export const Notifications = (): ReactElement => {
- {/* TODO: Confirm order */} { - setPreferences((prev) => ({ - ...prev, + setNotificationPreferences({ + ...notificationPreferences, [WebhookType.INCOMING_ETHER]: checked, [WebhookType.INCOMING_TOKEN]: checked, - })) + }) }} + disabled={!isSafeRegistered} /> } label="Incoming assets" @@ -140,14 +138,18 @@ export const Notifications = (): ReactElement => { { - setPreferences((prev) => ({ - ...prev, + setNotificationPreferences({ + ...notificationPreferences, [WebhookType.OUTGOING_ETHER]: checked, [WebhookType.OUTGOING_TOKEN]: checked, - })) + }) }} + disabled={!isSafeRegistered} /> } label="Outgoing assets" @@ -155,13 +157,14 @@ export const Notifications = (): ReactElement => { { - setPreferences((prev) => ({ - ...prev, + setNotificationPreferences({ + ...notificationPreferences, [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, - })) + }) }} + disabled={!isSafeRegistered} /> } label="Pending transactions" @@ -170,61 +173,80 @@ export const Notifications = (): ReactElement => { { - setPreferences((prev) => ({ - ...prev, - [WebhookType.CONFIRMATION_REQUEST]: checked, - })) + setNotificationPreferences({ + ...notificationPreferences, + [WebhookType.NEW_CONFIRMATION]: checked, + }) }} + disabled={!isSafeRegistered} /> } - label="Confirmation requests" + label="New confirmations" /> { - setPreferences((prev) => ({ - ...prev, - [WebhookType.NEW_CONFIRMATION]: checked, - })) + setNotificationPreferences({ + ...notificationPreferences, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, + }) }} + disabled={!isSafeRegistered} /> } - label="New confirmations" + label="Executed transactions" /> { - setPreferences((prev) => ({ - ...prev, - [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, - })) + setNotificationPreferences({ + ...notificationPreferences, + [WebhookType.MODULE_TRANSACTION]: checked, + }) }} + disabled={!isSafeRegistered} /> } - label="Executed transactions" + label="Module transactions" /> { - setPreferences((prev) => ({ - ...prev, - [WebhookType.MODULE_TRANSACTION]: checked, - })) + registerNotificationDevice({ + deviceUuid, + safesToRegister: { + [safe.chainId]: [safe.address.value], + }, + web3, // Add signature + callback: () => + setNotificationPreferences({ + ...notificationPreferences, + [WebhookType.CONFIRMATION_REQUEST]: checked, + }), + }) }} /> } - label="Module transactions" + label={ + <> + Confirmation requests + + {isOwner ? 'Requires your signature' : 'Only owners'} + + + } + disabled={!isOwner || !isSafeRegistered} /> diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/Notifications/logic.test.ts index dd5822a440..471622a76c 100644 --- a/src/components/settings/Notifications/logic.test.ts +++ b/src/components/settings/Notifications/logic.test.ts @@ -1,13 +1,6 @@ -import * as firebase from 'firebase/messaging' -import { hexZeroPad } from 'ethers/lib/utils' import { Web3Provider } from '@ethersproject/providers' -import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' -import * as sdk from '@safe-global/safe-gateway-typescript-sdk' -import type { JsonRpcSigner } from '@ethersproject/providers' -import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import * as logic from './logic' -import packageJson from '../../../../package.json' jest.mock('firebase/messaging') @@ -94,369 +87,371 @@ describe('Notifications', () => { }) }) - describe('createRegisterDevicePayload', () => { - it('should return the current registration if it is the same', async () => { - const token = crypto.randomUUID() - const signature = hexZeroPad('0xDEAD', 65) - - jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - jest.spyOn(mockProvider, 'getSigner').mockImplementation( - () => - ({ - signMessage: jest.fn().mockResolvedValue(signature), - } as unknown as JsonRpcSigner), - ) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: token, - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - const payload = await logic.createRegisterDevicePayload( - { [chainId]: [safeAddress] }, - mockProvider, - currentRegistration, - ) - - expect(payload).toStrictEqual({ ...currentRegistration, timestamp: expect.any(String) }) - }) - - describe('should return a registration payload if the chain registration(s) is not already registered', () => { - it('if none on the same chain is registered', async () => { - const token = crypto.randomUUID() - const signature = hexZeroPad('0xDEAD', 65) - - jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - jest.spyOn(mockProvider, 'getSigner').mockImplementation( - () => - ({ - signMessage: jest.fn().mockResolvedValue(signature), - } as unknown as JsonRpcSigner), - ) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const payload = await logic.createRegisterDevicePayload({ [chainId]: [safeAddress] }, mockProvider) - - expect(payload).toStrictEqual({ - uuid: expect.any(String), - cloudMessagingToken: token, - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: expect.any(String), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [signature], - }, - ], - }) - }) - - it('if others on the same chain exists', async () => { - const token = crypto.randomUUID() - const signature = hexZeroPad('0xDEAD', 65) - - jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - jest.spyOn(mockProvider, 'getSigner').mockImplementation( - () => - ({ - signMessage: jest.fn().mockResolvedValue(signature), - } as unknown as JsonRpcSigner), - ) - - const safeAddress = hexZeroPad('0x3', 20) - const chainId = '1' - - const currentRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: token, - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: expect.any(String), - safeRegistrations: [ - { - chainId, - safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], - signatures: [hexZeroPad('0xBEEF', 65)], - }, - { - chainId: '2', - safes: [hexZeroPad('0x4', 20)], - signatures: [signature], - }, - ], - } - - const payload = await logic.createRegisterDevicePayload( - { [chainId]: [safeAddress] }, - mockProvider, - currentRegistration, - ) - - expect(payload.timestamp).not.toBe(currentRegistration.timestamp) - expect(payload.safeRegistrations[0].signatures).not.toBe(currentRegistration.safeRegistrations[0].signatures) - - expect(payload).toStrictEqual({ - uuid: currentRegistration.uuid, // Same UUID - cloudMessagingToken: expect.any(String), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: expect.any(String), - safeRegistrations: [ - { - chainId, - safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20), safeAddress], - signatures: [signature], - }, - { - chainId: '2', - safes: [hexZeroPad('0x4', 20)], - signatures: [signature], - }, - ], - }) - }) - }) - }) - - describe('registerNotifications', () => { - const mockRegisterSafe = jest.spyOn(sdk, 'registerDevice') - - it('should return undefined if no registration exists and the registration threw', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - jest - .spyOn(logic, 'createRegisterDevicePayload') - .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - mockRegisterSafe.mockImplementation(() => { - return Promise.reject() - }) - - const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) - - expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') - - expect(registration).toBe(undefined) - }) - - it('should return the current registration if one exists and the registration threw', async () => { - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - jest - .spyOn(logic, 'createRegisterDevicePayload') - .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - mockRegisterSafe.mockImplementation(() => { - return Promise.reject() - }) - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - const registration = await logic.registerNotifications( - { [chainId]: [safeAddress] }, - mockProvider, - currentRegistration, - ) - - expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') - - expect(registration).toBe(currentRegistration) - }) - - it('should return the registration payload if the registration succeeded', async () => { - jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const registrationPayload: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - jest.spyOn(logic, 'createRegisterDevicePayload').mockImplementation(() => Promise.resolve(registrationPayload)) - - mockRegisterSafe.mockImplementation(() => { - return Promise.resolve() - }) - - const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) - - expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - expect(alertMock).not.toHaveBeenCalled() - - expect(registration).not.toBe(registrationPayload) - }) - }) - - describe('unregisterSafe', () => { - const mockUnregisterSafe = jest.spyOn(sdk, 'unregisterSafe') - - it('should return the current registration if the unregistration threw', async () => { - mockUnregisterSafe.mockImplementation(() => { - return Promise.reject() - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - ], - } - - const updatedRegistration = await logic.unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) - - expect(alertMock).toHaveBeenCalledTimes(1) - expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') - - expect(updatedRegistration).toEqual(currentRegistration) - }) - - it('should return the updated registration if the registration succeeded', async () => { - mockUnregisterSafe.mockImplementation(() => { - return Promise.resolve() - }) - - const safeAddress = hexZeroPad('0x1', 20) - const chainId = '1' - - const currentRegistration: logic.NotificationRegistration = { - uuid: crypto.randomUUID(), - cloudMessagingToken: crypto.randomUUID(), - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: crypto.randomUUID(), - safeRegistrations: [ - { - chainId, - safes: [safeAddress, hexZeroPad('0x2', 20)], - signatures: [hexZeroPad('0xDEAD', 65)], - }, - { - chainId: '5', - safes: [safeAddress], // Same address Safe on a different chain - signatures: [hexZeroPad('0xBEEF', 65)], - }, - ], - } - - const updatedRegistration = await logic.unregisterSafe( - { chainId, address: { value: safeAddress } } as SafeInfo, - currentRegistration, - ) - - expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) - - expect(alertMock).not.toHaveBeenCalled() - - expect(updatedRegistration.timestamp).not.toBe(currentRegistration.timestamp) - - expect(updatedRegistration).toEqual({ - uuid: currentRegistration.uuid, // Same UUID - cloudMessagingToken: currentRegistration.cloudMessagingToken, // Same token - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: expect.any(String), - safeRegistrations: [ - { - chainId, - safes: [hexZeroPad('0x2', 20)], - signatures: [], - }, - { - chainId: '5', - safes: [safeAddress], // Same address Safe on a different chain - signatures: [hexZeroPad('0xBEEF', 65)], - }, - ], - }) - }) - }) + // TODO: + + // describe('createRegisterDevicePayload', () => { + // it('should return the current registration if it is the same', async () => { + // const token = crypto.randomUUID() + // const signature = hexZeroPad('0xDEAD', 65) + + // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + // jest.spyOn(mockProvider, 'getSigner').mockImplementation( + // () => + // ({ + // signMessage: jest.fn().mockResolvedValue(signature), + // } as unknown as JsonRpcSigner), + // ) + + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // const currentRegistration: logic.NotificationRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: token, + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: crypto.randomUUID(), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress], + // signatures: [hexZeroPad('0xDEAD', 65)], + // }, + // ], + // } + + // const payload = await logic.createRegisterDevicePayload( + // { [chainId]: [safeAddress] }, + // mockProvider, + // currentRegistration, + // ) + + // expect(payload).toStrictEqual({ ...currentRegistration, timestamp: expect.any(String) }) + // }) + + // describe('should return a registration payload if the chain registration(s) is not already registered', () => { + // it('if none on the same chain is registered', async () => { + // const token = crypto.randomUUID() + // const signature = hexZeroPad('0xDEAD', 65) + + // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + // jest.spyOn(mockProvider, 'getSigner').mockImplementation( + // () => + // ({ + // signMessage: jest.fn().mockResolvedValue(signature), + // } as unknown as JsonRpcSigner), + // ) + + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // const payload = await logic.createRegisterDevicePayload({ [chainId]: [safeAddress] }, mockProvider) + + // expect(payload).toStrictEqual({ + // uuid: expect.any(String), + // cloudMessagingToken: token, + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: expect.any(String), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress], + // signatures: [signature], + // }, + // ], + // }) + // }) + + // it('if others on the same chain exists', async () => { + // const token = crypto.randomUUID() + // const signature = hexZeroPad('0xDEAD', 65) + + // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + // jest.spyOn(mockProvider, 'getSigner').mockImplementation( + // () => + // ({ + // signMessage: jest.fn().mockResolvedValue(signature), + // } as unknown as JsonRpcSigner), + // ) + + // const safeAddress = hexZeroPad('0x3', 20) + // const chainId = '1' + + // const currentRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: token, + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: expect.any(String), + // safeRegistrations: [ + // { + // chainId, + // safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + // signatures: [hexZeroPad('0xBEEF', 65)], + // }, + // { + // chainId: '2', + // safes: [hexZeroPad('0x4', 20)], + // signatures: [signature], + // }, + // ], + // } + + // const payload = await logic.createRegisterDevicePayload( + // { [chainId]: [safeAddress] }, + // mockProvider, + // currentRegistration, + // ) + + // expect(payload.timestamp).not.toBe(currentRegistration.timestamp) + // expect(payload.safeRegistrations[0].signatures).not.toBe(currentRegistration.safeRegistrations[0].signatures) + + // expect(payload).toStrictEqual({ + // uuid: currentRegistration.uuid, // Same UUID + // cloudMessagingToken: expect.any(String), + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: expect.any(String), + // safeRegistrations: [ + // { + // chainId, + // safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20), safeAddress], + // signatures: [signature], + // }, + // { + // chainId: '2', + // safes: [hexZeroPad('0x4', 20)], + // signatures: [signature], + // }, + // ], + // }) + // }) + // }) + // }) + + // describe('registerNotifications', () => { + // const mockRegisterSafe = jest.spyOn(sdk, 'registerDevice') + + // it('should return undefined if no registration exists and the registration threw', async () => { + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + + // jest + // .spyOn(logic, 'createRegisterDevicePayload') + // .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) + + // mockRegisterSafe.mockImplementation(() => { + // return Promise.reject() + // }) + + // const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) + + // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) + + // expect(alertMock).toHaveBeenCalledTimes(1) + // expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') + + // expect(registration).toBe(undefined) + // }) + + // it('should return the current registration if one exists and the registration threw', async () => { + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + + // jest + // .spyOn(logic, 'createRegisterDevicePayload') + // .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) + + // mockRegisterSafe.mockImplementation(() => { + // return Promise.reject() + // }) + + // const currentRegistration: logic.NotificationRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: crypto.randomUUID(), + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: crypto.randomUUID(), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress], + // signatures: [hexZeroPad('0xDEAD', 65)], + // }, + // ], + // } + + // const registration = await logic.registerNotifications( + // { [chainId]: [safeAddress] }, + // mockProvider, + // currentRegistration, + // ) + + // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) + + // expect(alertMock).toHaveBeenCalledTimes(1) + // expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') + + // expect(registration).toBe(currentRegistration) + // }) + + // it('should return the registration payload if the registration succeeded', async () => { + // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) + + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // const registrationPayload: logic.NotificationRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: crypto.randomUUID(), + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: crypto.randomUUID(), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress], + // signatures: [hexZeroPad('0xDEAD', 65)], + // }, + // ], + // } + + // jest.spyOn(logic, 'createRegisterDevicePayload').mockImplementation(() => Promise.resolve(registrationPayload)) + + // mockRegisterSafe.mockImplementation(() => { + // return Promise.resolve() + // }) + + // const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) + + // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) + + // expect(alertMock).not.toHaveBeenCalled() + + // expect(registration).not.toBe(registrationPayload) + // }) + // }) + + // describe('unregisterSafe', () => { + // const mockUnregisterSafe = jest.spyOn(sdk, 'unregisterSafe') + + // it('should return the current registration if the unregistration threw', async () => { + // mockUnregisterSafe.mockImplementation(() => { + // return Promise.reject() + // }) + + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // const currentRegistration: logic.NotificationRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: crypto.randomUUID(), + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: crypto.randomUUID(), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress], + // signatures: [hexZeroPad('0xDEAD', 65)], + // }, + // ], + // } + + // const updatedRegistration = await logic.unregisterSafe( + // { chainId, address: { value: safeAddress } } as SafeInfo, + // currentRegistration, + // ) + + // expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) + + // expect(alertMock).toHaveBeenCalledTimes(1) + // expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') + + // expect(updatedRegistration).toEqual(currentRegistration) + // }) + + // it('should return the updated registration if the registration succeeded', async () => { + // mockUnregisterSafe.mockImplementation(() => { + // return Promise.resolve() + // }) + + // const safeAddress = hexZeroPad('0x1', 20) + // const chainId = '1' + + // const currentRegistration: logic.NotificationRegistration = { + // uuid: crypto.randomUUID(), + // cloudMessagingToken: crypto.randomUUID(), + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: crypto.randomUUID(), + // safeRegistrations: [ + // { + // chainId, + // safes: [safeAddress, hexZeroPad('0x2', 20)], + // signatures: [hexZeroPad('0xDEAD', 65)], + // }, + // { + // chainId: '5', + // safes: [safeAddress], // Same address Safe on a different chain + // signatures: [hexZeroPad('0xBEEF', 65)], + // }, + // ], + // } + + // const updatedRegistration = await logic.unregisterSafe( + // { chainId, address: { value: safeAddress } } as SafeInfo, + // currentRegistration, + // ) + + // expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) + + // expect(alertMock).not.toHaveBeenCalled() + + // expect(updatedRegistration.timestamp).not.toBe(currentRegistration.timestamp) + + // expect(updatedRegistration).toEqual({ + // uuid: currentRegistration.uuid, // Same UUID + // cloudMessagingToken: currentRegistration.cloudMessagingToken, // Same token + // buildNumber: '0', + // bundle: 'https://app.safe.global', + // deviceType: DeviceType.WEB, + // version: packageJson.version, + // timestamp: expect.any(String), + // safeRegistrations: [ + // { + // chainId, + // safes: [hexZeroPad('0x2', 20)], + // signatures: [], + // }, + // { + // chainId: '5', + // safes: [safeAddress], // Same address Safe on a different chain + // signatures: [hexZeroPad('0xBEEF', 65)], + // }, + // ], + // }) + // }) + // }) }) diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts index 5af6ba5ebe..a606ed431d 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/Notifications/logic.ts @@ -1,8 +1,7 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { getToken, getMessaging } from 'firebase/messaging' -import { registerDevice, unregisterSafe as gatewayUnregisterSafe } from '@safe-global/safe-gateway-typescript-sdk' +import { registerDevice, unregisterSafe } from '@safe-global/safe-gateway-typescript-sdk' import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' -import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' @@ -36,25 +35,50 @@ export const requestNotificationPermission = async (): Promise => { return isGranted } -const getTimestampWithoutMilliseconds = () => { - return Math.floor(new Date().getTime() / 1000).toString() -} - -export const createRegisterDevicePayload = async ( - safesToRegister: { [chainId: string]: Array }, - web3: Web3Provider, - currentRegistration?: NotificationRegistration, -): Promise => { +const getSafeRegistrationSignature = ({ + safes, + web3, + timestamp, + deviceUuid, + token, +}: { + safes: Array + web3: Web3Provider + timestamp: string + deviceUuid: string + token: string +}) => { const MESSAGE_PREFIX = 'gnosis-safe' - for (const { chainId, safes } of currentRegistration?.safeRegistrations ?? []) { - if (safesToRegister[chainId]) { - safesToRegister[chainId] = Array.from(new Set([...safes, ...safesToRegister[chainId]])) - } else { - safesToRegister[chainId] = safes + const message = MESSAGE_PREFIX + timestamp + deviceUuid + token + safes.join('') + const hashedMessage = keccak256(toUtf8Bytes(message)) + + return web3.getSigner().signMessage(hashedMessage) +} + +type RegisterDeviceParams = + | { + safesToRegister: { [chainId: string]: Array } + deviceUuid: string + callback: () => void + web3?: never + } + | { + safesToRegister: { [chainId: string]: Array } + deviceUuid: string + callback?: () => void + web3: Web3Provider } - } +export const getRegisterDevicePayload = async ({ + safesToRegister, + deviceUuid, + web3, +}: { + safesToRegister: { [chainId: string]: Array } + deviceUuid: string + web3?: Web3Provider +}): Promise => { const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) // Get Firebase token @@ -73,27 +97,25 @@ export const createRegisterDevicePayload = async ( // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 - const timestamp = getTimestampWithoutMilliseconds() - const uuid = currentRegistration?.uuid ?? self.crypto.randomUUID() + const timestamp = Math.floor(new Date().getTime() / 1000).toString() const safeRegistrations = await Promise.all( - Object.entries(safesToRegister) - .filter(([, safes]) => safes.length > 0) - .map(async ([chainId, safes]) => { - const message = MESSAGE_PREFIX + timestamp + uuid + token + safes.join('') - const hashedMessage = keccak256(toUtf8Bytes(message)) - const signature = await web3.getSigner().signMessage(hashedMessage) - - return { - chainId, - safes, - signatures: [signature], - } - }), + Object.entries(safesToRegister).map(async ([chainId, safeAddresses]) => { + // Signature is only required for CONFIRMATION_REQUESTS + const signature = web3 + ? await getSafeRegistrationSignature({ safes: safeAddresses, web3, deviceUuid, timestamp, token }) + : undefined + + return { + chainId, + safes: safeAddresses, + signatures: signature ? [signature] : [], + } + }), ) return { - uuid, + uuid: deviceUuid, cloudMessagingToken: token, buildNumber: '0', // Required value, but does not exist on web bundle: location.origin, @@ -104,80 +126,53 @@ export const createRegisterDevicePayload = async ( } } -export const registerNotifications = async ( - safesToRegister: { [chainId: string]: Array }, - web3: Web3Provider, - currentRegistration?: NotificationRegistration, -): Promise => { - const isGranted = await requestNotificationPermission() - - if (!isGranted) { - return currentRegistration - } - +export const registerNotificationDevice = async ({ + safesToRegister, + deviceUuid, + callback, + web3, +}: RegisterDeviceParams) => { let didRegister = false - let payload: NotificationRegistration | undefined - try { - payload = await createRegisterDevicePayload(safesToRegister, web3, currentRegistration) + const payload = await getRegisterDevicePayload({ deviceUuid, safesToRegister, web3 }) - if (payload) { - // 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) + // 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 - } + didRegister = response == null } catch (e) { - console.error('Error registering Safe(s)', e) + console.error(`Error registering Safe(s)`, e) } - if (!didRegister) { - alert('Unable to register Safe(s)') - return currentRegistration + if (didRegister) { + callback?.() } - - return payload } -export const unregisterSafe = async ( - safe: SafeInfo, - currentRegistration: NotificationRegistration, -): Promise => { +export const unregisterSafeNotifications = async ({ + chainId, + safeAddress, + deviceUuid, + callback, +}: { + chainId: string + safeAddress: string + deviceUuid: string + callback: () => void +}) => { let didUnregister = false try { - const response = await gatewayUnregisterSafe(safe.chainId, safe.address.value, currentRegistration.uuid) + const response = await unregisterSafe(chainId, safeAddress, deviceUuid) didUnregister = response == null } catch (e) { - console.error('Error unregistering Safe', e) - } - - if (!didUnregister) { - alert('Unable to unregister Safe') - return currentRegistration + console.error(`Error unregistering ${safeAddress} on chain ${chainId}`, e) } - // Remove deleted Safe from registration and clear signatures - const updatedSafeRegistrations = currentRegistration.safeRegistrations.map((registration) => { - if (registration.chainId !== safe.chainId) { - return registration - } - - const updatedSafes = registration.safes.filter((safeAddress) => safeAddress !== safe.address.value) - - return { - ...registration, - safes: updatedSafes, - signatures: [], - } - }) - - return { - ...currentRegistration, - timestamp: getTimestampWithoutMilliseconds(), - safeRegistrations: updatedSafeRegistrations, + if (didUnregister) { + callback() } } diff --git a/src/components/settings/Notifications/useNotificationDb.ts b/src/components/settings/Notifications/useNotificationDb.ts new file mode 100644 index 0000000000..1875c7519f --- /dev/null +++ b/src/components/settings/Notifications/useNotificationDb.ts @@ -0,0 +1,152 @@ +import { useEffect, useMemo, useState } from 'react' +import { del, delMany, entries, set, setMany } from 'idb-keyval' + +import { WebhookType } from '@/services/firebase/webhooks' +import useSafeInfo from '@/hooks/useSafeInfo' +import { getNotificationDbKey, createNotificationDbStore } from '@/services/firebase/notification-db' +import type { NotificationDbKey } from '@/services/firebase/notification-db' + +const UUID_KEY = '__uuid' + +const defaultNotificationPreferences = { + [WebhookType.NEW_CONFIRMATION]: true, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, + [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, + [WebhookType.INCOMING_ETHER]: true, + [WebhookType.OUTGOING_ETHER]: true, + [WebhookType.INCOMING_TOKEN]: true, + [WebhookType.OUTGOING_TOKEN]: true, + [WebhookType.MODULE_TRANSACTION]: true, + [WebhookType.CONFIRMATION_REQUEST]: false, // Requires signature +} + +export const useNotificationDb = () => { + const { safe } = useSafeInfo() + + const SAFE_STORE_NAME = + safe.chainId && safe.address.value ? getNotificationDbKey(safe.chainId, safe.address.value) : undefined + + const [notificationDb, setNotificationDb] = useState< + Record & { [UUID_KEY]: string } + >({ + [UUID_KEY]: '', + }) + + const store = useMemo(() => { + if (typeof window !== 'undefined') { + return createNotificationDbStore() + } + }, []) + + // Load database + useEffect(() => { + if (!store) { + return + } + + entries(store) + .then(async (entries) => { + const db = Object.fromEntries(entries) + + // Set UUID if it does not exist + if (!db[UUID_KEY]) { + const uuid = self.crypto.randomUUID() + db[UUID_KEY] = uuid + + set(UUID_KEY, uuid, store) + } + + setNotificationDb((prev) => ({ ...prev, ...db })) + }) + .catch(() => null) + }, [store]) + + const notificationPreferences = SAFE_STORE_NAME ? notificationDb[SAFE_STORE_NAME] : undefined + + const setNotificationPreferences = (preferences: typeof defaultNotificationPreferences) => { + if (!SAFE_STORE_NAME) { + return + } + + const db = { + ...notificationDb, + [SAFE_STORE_NAME]: { + ...notificationPreferences, + preferences, + }, + } + + setMany(Object.entries(db), store) + .then(() => { + setNotificationDb((prev) => ({ ...prev, [SAFE_STORE_NAME]: preferences })) + }) + .catch(() => null) + } + + const locallyRegisteredSafes = useMemo(() => { + const safes: { [chainId: string]: Array } = {} + + for (const key of Object.keys(notificationDb)) { + if (key === UUID_KEY) { + continue + } + + const [chainId, address] = key.split(':') + + if (!safes[chainId]) { + safes[chainId] = [] + } + + safes[chainId].push(address) + } + + return safes + }, [notificationDb]) + + const registerSafeLocally = (chainId: string, safeAddress: string) => { + const key = getNotificationDbKey(chainId, safeAddress) + + set(key, defaultNotificationPreferences, store) + .then(() => { + setNotificationDb((prev) => ({ ...prev, [key]: defaultNotificationPreferences })) + }) + .catch(() => null) + } + + const unregisterSafeLocally = (chainId: string, safeAddress: string) => { + const key = getNotificationDbKey(chainId, safeAddress) + + del(key, store) + .then(() => { + setNotificationDb((prev) => { + delete prev[key] + + return prev + }) + }) + .catch(() => null) + } + + const clearLocallyRegisteredSafes = () => { + const keys = Object.keys(notificationDb).filter((key) => key !== UUID_KEY) + + delMany(keys, store) + .then(() => { + setNotificationDb(({ [UUID_KEY]: uuid }) => { + return { [UUID_KEY]: uuid } + }) + }) + .catch(() => null) + } + + return { + deviceUuid: notificationDb[UUID_KEY], + notificationPreferences: notificationPreferences ?? defaultNotificationPreferences, + setNotificationPreferences, + isSafeRegistered: !!notificationPreferences, + locallyRegisteredSafes, + registerSafeLocally, + unregisterSafeLocally, + clearLocallyRegisteredSafes, + } +} diff --git a/src/components/settings/Notifications/useNotificationPreferences.ts b/src/components/settings/Notifications/useNotificationPreferences.ts deleted file mode 100644 index b584239107..0000000000 --- a/src/components/settings/Notifications/useNotificationPreferences.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { entries, setMany } from 'idb-keyval' - -import { WebhookType } from '@/services/firebase/webhooks' -import { getNotificationPreferencesStore } from '@/services/firebase' -import useSafeInfo from '@/hooks/useSafeInfo' - -type NotifcationPreferences = { [key in WebhookType]: boolean } - -const getDefaultPreferences = (): NotifcationPreferences => { - return Object.values(WebhookType).reduce((acc, type) => { - acc[type] = true - return acc - }, {} as NotifcationPreferences) -} - -export const useNotificationPreferences = () => { - const { safe } = useSafeInfo() - const [preferences, _setPreferences] = useState(getDefaultPreferences) - - const customStore = useMemo(() => { - if (typeof window !== 'undefined' && safe.address.value) { - return getNotificationPreferencesStore(safe.address.value) - } - }, [safe.address.value]) - - useEffect(() => { - if (!customStore) { - return - } - - entries(customStore).then((entries) => { - const _preferences = Object.fromEntries(entries) - - _setPreferences((prev) => ({ ...prev, ..._preferences })) - }) - }, [customStore]) - - const setPreferences: typeof _setPreferences = useCallback( - (value) => { - const newValue = value instanceof Function ? value(preferences) : value - - setMany(Object.entries(newValue), customStore).then(() => { - _setPreferences((prev) => ({ ...prev, ...newValue })) - }) - }, - [customStore, preferences], - ) - - return [preferences, setPreferences] as const -} diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 23409aa815..f088a7c125 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -1,5 +1,5 @@ import { formatUnits } from 'ethers/lib/utils' -import { createStore, get } from 'idb-keyval' +import { get } from 'idb-keyval' import type { MessagePayload } from 'firebase/messaging/sw' import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' @@ -8,22 +8,21 @@ import { AppRoutes } from '@/config/routes' import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' import type { WebhookEvent } from '@/services/firebase/webhooks' import { GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' - -export const getNotificationPreferencesStore = (safeAddress: string) => { - const DB_NAME = 'notification-preferences' - - return createStore(DB_NAME, safeAddress) -} +import { createNotificationDbStore, getNotificationDbKey } from './notification-db' export const shouldShowNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { return true } - const store = getNotificationPreferencesStore(payload.data.address) - const preference = await get(payload.data.type, store) + const { chainId, address, type } = payload.data + + const key = getNotificationDbKey(chainId, address) + const store = createNotificationDbStore() + + const safeStore = await get(key, store).catch(() => null) - return preference ?? true + return safeStore?.[type] ?? true } // localStorage cannot be accessed in service workers so we reference the flag diff --git a/src/services/firebase/notification-db.ts b/src/services/firebase/notification-db.ts new file mode 100644 index 0000000000..9f524449d6 --- /dev/null +++ b/src/services/firebase/notification-db.ts @@ -0,0 +1,14 @@ +import { createStore } from 'idb-keyval' + +export type NotificationDbKey = `${string}:${string}` + +export const getNotificationDbKey = (chainId: string, safeAddress: string): NotificationDbKey => { + return `${chainId}:${safeAddress}` +} + +export const createNotificationDbStore = () => { + const DB_NAME = 'notifications-database' + const STORE_NAME = 'notifications-store' + + return createStore(DB_NAME, STORE_NAME) +} From b71acd5a35889a865280b468937fa447ab4bbe9c Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 23 Aug 2023 18:23:07 +0200 Subject: [PATCH 17/62] fix: adjust style, add banner + registration list --- .../Notifications/AllSafesNotifications.tsx | 86 ++++++++++++------- .../settings/Notifications/index.tsx | 53 +++++++++--- .../settings/Notifications/styles.module.css | 8 ++ 3 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/components/settings/Notifications/AllSafesNotifications.tsx b/src/components/settings/Notifications/AllSafesNotifications.tsx index 1d03ed439c..823afe125e 100644 --- a/src/components/settings/Notifications/AllSafesNotifications.tsx +++ b/src/components/settings/Notifications/AllSafesNotifications.tsx @@ -18,15 +18,18 @@ import type { ReactElement } from 'react' import EthHashInfo from '@/components/common/EthHashInfo' import { sameAddress } from '@/utils/addresses' import useChains from '@/hooks/useChains' -import { useAppSelector } from '@/store' -import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' +import { useAppDispatch, useAppSelector } from '@/store' +import { selectAllAddedSafes } from '@/store/addedSafesSlice' import CheckWallet from '@/components/common/CheckWallet' import { registerNotificationDevice, requestNotificationPermission, unregisterSafeNotifications } from './logic' import { useNotificationDb } from './useNotificationDb' +import { showNotification } from '@/store/notificationsSlice' + +import css from './styles.module.css' export const AllSafesNotifications = (): ReactElement | null => { const chains = useChains() - const totalAddedSafes = useAppSelector(selectTotalAdded) + const dispatch = useAppDispatch() const addedSafes = useAppSelector(selectAllAddedSafes) const { @@ -37,7 +40,7 @@ export const AllSafesNotifications = (): ReactElement | null => { clearLocallyRegisteredSafes, } = useNotificationDb() - const [selectedSafes, setSelectedSafes] = useState(() => locallyRegisteredSafes) + const [selectedSafes, setSelectedSafes] = useState(locallyRegisteredSafes) // `locallyRegisteredSafes` is initially undefined until indexedDB resolves useEffect(() => { @@ -54,17 +57,19 @@ export const AllSafesNotifications = (): ReactElement | null => { } // Locally registered Safes (if not already added) - for (const safeAddress of Object.keys(locallyRegisteredSafes)) { - const [chainId, address] = safeAddress.split(':') - - if (chainId && address) { - registerable[chainId] = Array.from(new Set([...registerable[chainId], address])) - } + for (const [chainId, safeAddresses] of Object.entries(locallyRegisteredSafes)) { + registerable[chainId] = Array.from(new Set([...registerable[chainId], ...safeAddresses])) } return registerable }, [addedSafes, locallyRegisteredSafes]) + const totalNotifiableSafes = useMemo(() => { + return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { + return (acc += safeAddresses.length) + }, 0) + }, [notifiableSafes]) + const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { const hasChain = Object.keys(selectedSafes).includes(chainId) const hasEverySafe = safeAddresses.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) @@ -115,7 +120,8 @@ export const AllSafesNotifications = (): ReactElement | null => { return } - const promises = [] + const registrationPromises = [] + const safesToRegister: { [chainId: string]: Array } = {} for (const [chainId, safeAddresses] of Object.entries(selectedSafes)) { @@ -123,7 +129,7 @@ export const AllSafesNotifications = (): ReactElement | null => { const shouldUnregister = locallyRegisteredSafes[chainId]?.includes(safeAddress) if (shouldUnregister) { - promises.push( + registrationPromises.push( unregisterSafeNotifications({ chainId, safeAddress: safeAddress, @@ -131,26 +137,38 @@ export const AllSafesNotifications = (): ReactElement | null => { callback: () => unregisterSafeLocally(chainId, safeAddress), }), ) - } else { - if (!safesToRegister[chainId]) { - safesToRegister[chainId] = [] - } + continue + } - safesToRegister[chainId].push(safeAddress) + // Safes to register + if (!safesToRegister[chainId]) { + safesToRegister[chainId] = [] } + + safesToRegister[chainId].push(safeAddress) } } - if (Object.keys(safesToRegister).length > 0) { + const shouldRegisterSafes = Object.keys(safesToRegister).length > 0 + + if (shouldRegisterSafes) { const callback = () => { Object.entries(safesToRegister).forEach(([chainId, safeAddresses]) => { safeAddresses.forEach((safeAddress) => { registerSafeLocally(chainId, safeAddress) }) }) + + dispatch( + showNotification({ + message: 'You will now receive notifications for these Safe Accounts in your browser.', + variant: 'success', + groupKey: 'notifications', + }), + ) } - promises.push( + registrationPromises.push( registerNotificationDevice({ safesToRegister, deviceUuid, @@ -159,18 +177,18 @@ export const AllSafesNotifications = (): ReactElement | null => { ) } - Promise.all(promises) + Promise.all(registrationPromises) } - if (totalAddedSafes === 0) { + if (totalNotifiableSafes === 0) { return null } return ( - + - My Safes ({totalAddedSafes}) + My Safes ({totalNotifiableSafes}) @@ -185,9 +203,9 @@ export const AllSafesNotifications = (): ReactElement | null => { `1px solid ${palette.border.light}` }}> - + - + @@ -223,16 +241,16 @@ export const AllSafesNotifications = (): ReactElement | null => { return ( - + - + - + {safeAddresses.map((address) => { const isSafeSelected = selectedSafes[chainId]?.includes(address) ?? false @@ -254,11 +272,17 @@ export const AllSafesNotifications = (): ReactElement | null => { return ( - - + + - + ) diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 016f56a06d..b609a97051 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,4 +1,15 @@ -import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch, Divider } from '@mui/material' +import { + Grid, + Paper, + Typography, + Checkbox, + FormControlLabel, + FormGroup, + Alert, + Switch, + Divider, + AlertTitle, +} from '@mui/material' import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' @@ -10,13 +21,19 @@ import { AllSafesNotifications } from './AllSafesNotifications' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { registerNotificationDevice, unregisterSafeNotifications } from './logic' import { useWeb3 } from '@/hooks/wallets/web3' +import { IS_DEV } from '@/config/constants' +import { useAppDispatch } from '@/store' +import { showNotification } from '@/store/notificationsSlice' import css from './styles.module.css' export const Notifications = (): ReactElement => { const web3 = useWeb3() + const dispatch = useAppDispatch() + const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() + const { deviceUuid, isSafeRegistered, @@ -26,6 +43,8 @@ export const Notifications = (): ReactElement => { unregisterSafeLocally, } = useNotificationDb() + const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') + const handleOnChange = () => { if (isSafeRegistered) { unregisterSafeNotifications({ @@ -38,7 +57,17 @@ export const Notifications = (): ReactElement => { registerNotificationDevice({ deviceUuid, safesToRegister: { [safe.chainId]: [safe.address.value] }, - callback: () => registerSafeLocally(safe.chainId, safe.address.value), + callback: () => { + registerSafeLocally(safe.chainId, safe.address.value) + + dispatch( + showNotification({ + message: 'You will now receive notifications for this Safe Account in your browser.', + variant: 'success', + groupKey: 'notification', + }), + ) + }, }) } } @@ -56,17 +85,17 @@ export const Notifications = (): ReactElement => { - {isSafeRegistered - ? 'You will receive notifications about this Safe Account in this browser.' - : `Subscribe to receive notifications about ${ - safeLoaded ? 'this Safe Account' : 'Safe Accounts' - } in this browser.`} + Enable push notifications for {safeLoaded ? 'this Safe Account' : 'your Safe Accounts'} in your browser. + You will need to enable them again if you clear your browser cache. - - Please note that registration is per-browser and you will need to register again if you clear your - browser cache. - + {(isMac || IS_DEV) && ( + + For MacOS users + Double-check that you have enabled your browser notifications under System Settings >{' '} + Notifications > Application Notifications (path may vary depending on OS version). + + )} {safeLoaded ? ( <> @@ -109,7 +138,7 @@ export const Notifications = (): ReactElement => { - Preferences + Notification diff --git a/src/components/settings/Notifications/styles.module.css b/src/components/settings/Notifications/styles.module.css index 20b056d7ca..eee7725502 100644 --- a/src/components/settings/Notifications/styles.module.css +++ b/src/components/settings/Notifications/styles.module.css @@ -20,3 +20,11 @@ .switch :global .MuiSwitch-track { background-color: var(--color-primary-main) !important; } + +.item { + padding-left: var(--space-1); +} + +.icon { + min-width: 38px; +} From 12d9c1d2fad709d3c60edc38d58cb85cc585ac92 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 24 Aug 2023 17:24:56 +0200 Subject: [PATCH 18/62] fix: split databases + finish design --- .../notifications/push-notification.svg | 11 + .../batch/BatchIndicator/BatchTooltip.tsx | 28 +- src/components/common/CustomTooltip/index.tsx | 22 ++ src/components/common/Header/index.tsx | 5 +- ...ifications.tsx => GlobalNotifications.tsx} | 242 +++++++++--------- .../NotificationBanner/index.tsx | 90 +++++++ .../NotificationBanner/styles.module.css | 57 +++++ .../Notifications/hooks/notifications-idb.ts | 31 +++ .../hooks/useNotificationPreferences.ts | 181 +++++++++++++ .../hooks/useNotificationRegistrations.ts | 88 +++++++ .../settings/Notifications/index.tsx | 182 +++++-------- .../settings/Notifications/logic.ts | 74 +----- .../settings/Notifications/styles.module.css | 8 - .../Notifications/useNotificationDb.ts | 152 ----------- src/pages/settings/notifications.tsx | 4 +- src/services/firebase/index.ts | 19 +- src/services/firebase/notification-db.ts | 14 - 17 files changed, 707 insertions(+), 501 deletions(-) create mode 100644 public/images/notifications/push-notification.svg create mode 100644 src/components/common/CustomTooltip/index.tsx rename src/components/settings/Notifications/{AllSafesNotifications.tsx => GlobalNotifications.tsx} (50%) create mode 100644 src/components/settings/Notifications/NotificationBanner/index.tsx create mode 100644 src/components/settings/Notifications/NotificationBanner/styles.module.css create mode 100644 src/components/settings/Notifications/hooks/notifications-idb.ts create mode 100644 src/components/settings/Notifications/hooks/useNotificationPreferences.ts create mode 100644 src/components/settings/Notifications/hooks/useNotificationRegistrations.ts delete mode 100644 src/components/settings/Notifications/useNotificationDb.ts delete mode 100644 src/services/firebase/notification-db.ts diff --git a/public/images/notifications/push-notification.svg b/public/images/notifications/push-notification.svg new file mode 100644 index 0000000000..7e5de5fd23 --- /dev/null +++ b/public/images/notifications/push-notification.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/batch/BatchIndicator/BatchTooltip.tsx b/src/components/batch/BatchIndicator/BatchTooltip.tsx index ebd71ddc74..9f9718886f 100644 --- a/src/components/batch/BatchIndicator/BatchTooltip.tsx +++ b/src/components/batch/BatchIndicator/BatchTooltip.tsx @@ -1,28 +1,9 @@ import { type ReactElement, useEffect, useState } from 'react' import { Box, SvgIcon } from '@mui/material' -import Tooltip, { type TooltipProps, tooltipClasses } from '@mui/material/Tooltip' -import { styled } from '@mui/material/styles' + import SuccessIcon from '@/public/images/common/success.svg' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' - -const StyledTooltip = styled(({ className, ...props }: TooltipProps) => ( - -))(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: theme.palette.background.paper, - color: theme.palette.text.primary, - fontSize: theme.typography.pxToRem(16), - fontWeight: 700, - border: `1px solid ${theme.palette.border.light}`, - marginTop: theme.spacing(2) + ' !important', - }, - [`& .${tooltipClasses.arrow}`]: { - color: theme.palette.background.paper, - }, - [`& .${tooltipClasses.arrow}:before`]: { - border: `1px solid ${theme.palette.border.light}`, - }, -})) +import { CustomTooltip } from '@/components/common/CustomTooltip' const BatchTooltip = ({ children }: { children: ReactElement }) => { const [showTooltip, setShowTooltip] = useState(false) @@ -40,10 +21,9 @@ const BatchTooltip = ({ children }: { children: ReactElement }) => { }, []) return ( - setShowTooltip(false)} - arrow title={ @@ -54,7 +34,7 @@ const BatchTooltip = ({ children }: { children: ReactElement }) => { } >
{children}
-
+ ) } diff --git a/src/components/common/CustomTooltip/index.tsx b/src/components/common/CustomTooltip/index.tsx new file mode 100644 index 0000000000..108c3bebea --- /dev/null +++ b/src/components/common/CustomTooltip/index.tsx @@ -0,0 +1,22 @@ +import { styled } from '@mui/material/styles' +import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' +import { type TooltipProps } from '@mui/material/Tooltip' + +export const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + fontSize: theme.typography.pxToRem(16), + fontWeight: 700, + border: `1px solid ${theme.palette.border.light}`, + marginTop: theme.spacing(2) + ' !important', + }, + [`& .${tooltipClasses.arrow}`]: { + color: theme.palette.background.paper, + }, + [`& .${tooltipClasses.arrow}:before`]: { + border: `1px solid ${theme.palette.border.light}`, + }, +})) diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index c2695ab811..63933da19b 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -15,6 +15,7 @@ import SafeLogo from '@/public/images/logo.svg' import Link from 'next/link' import useSafeAddress from '@/hooks/useSafeAddress' import BatchIndicator from '@/components/batch/BatchIndicator' +import { NotificationBanner } from '@/components/settings/Notifications/NotificationBanner' type HeaderProps = { onMenuToggle?: Dispatch> @@ -71,7 +72,9 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => {
- + + +
diff --git a/src/components/settings/Notifications/AllSafesNotifications.tsx b/src/components/settings/Notifications/GlobalNotifications.tsx similarity index 50% rename from src/components/settings/Notifications/AllSafesNotifications.tsx rename to src/components/settings/Notifications/GlobalNotifications.tsx index 823afe125e..00cd984738 100644 --- a/src/components/settings/Notifications/AllSafesNotifications.tsx +++ b/src/components/settings/Notifications/GlobalNotifications.tsx @@ -11,58 +11,96 @@ import { ListItemIcon, ListItemText, } from '@mui/material' -import { unregisterDevice } from '@safe-global/safe-gateway-typescript-sdk' import { Fragment, useEffect, useMemo, useState } from 'react' import type { ReactElement } from 'react' import EthHashInfo from '@/components/common/EthHashInfo' import { sameAddress } from '@/utils/addresses' import useChains from '@/hooks/useChains' -import { useAppDispatch, useAppSelector } from '@/store' -import { selectAllAddedSafes } from '@/store/addedSafesSlice' +import { useAppSelector } from '@/store' import CheckWallet from '@/components/common/CheckWallet' -import { registerNotificationDevice, requestNotificationPermission, unregisterSafeNotifications } from './logic' -import { useNotificationDb } from './useNotificationDb' -import { showNotification } from '@/store/notificationsSlice' +import { requestNotificationPermission } from './logic' +import { useNotificationPreferences } from './hooks/useNotificationPreferences' +import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' +import { selectAllAddedSafes } from '@/store/addedSafesSlice' +import type { AddedSafesState } from '@/store/addedSafesSlice' +import type { NotificationPreferences } from './hooks/notifications-idb' import css from './styles.module.css' -export const AllSafesNotifications = (): ReactElement | null => { - const chains = useChains() - const dispatch = useAppDispatch() - const addedSafes = useAppSelector(selectAllAddedSafes) +type NotifiableSafes = { [chainId: string]: Array } - const { - deviceUuid, - locallyRegisteredSafes, - registerSafeLocally, - unregisterSafeLocally, - clearLocallyRegisteredSafes, - } = useNotificationDb() +// Convert data structure of added Safes +export const getNotiableAddedSafes = (addedSafes: AddedSafesState): NotifiableSafes => { + const notifiableAddedSafes: NotifiableSafes = {} - const [selectedSafes, setSelectedSafes] = useState(locallyRegisteredSafes) + for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { + notifiableAddedSafes[chainId] = Object.keys(addedSafesOnChain) + } - // `locallyRegisteredSafes` is initially undefined until indexedDB resolves - useEffect(() => { - setSelectedSafes(locallyRegisteredSafes) - }, [locallyRegisteredSafes]) + return notifiableAddedSafes +} - // Merge added Safes and locally notification-registered Safes - const notifiableSafes = useMemo(() => { - const registerable: { [chainId: string]: Array } = {} +// Convert data structure of locally notification-registered Safes +const getCurrentNotifiedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { + const currentNotifiedSafes: NotifiableSafes = {} - // Added Safes - for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { - registerable[chainId] = Object.keys(addedSafesOnChain) + for (const { chainId, safeAddress } of Object.values(allPreferences)) { + if (!currentNotifiedSafes[chainId]) { + currentNotifiedSafes[chainId] = [] } - // Locally registered Safes (if not already added) - for (const [chainId, safeAddresses] of Object.entries(locallyRegisteredSafes)) { - registerable[chainId] = Array.from(new Set([...registerable[chainId], ...safeAddresses])) + currentNotifiedSafes[chainId].push(safeAddress) + } + + return currentNotifiedSafes +} + +const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions?: NotifiableSafes): NotifiableSafes => { + const notifiableSafes = getNotiableAddedSafes(addedSafes) + + if (!currentSubscriptions) { + return notifiableSafes + } + + // Locally registered Safes (if not already added) + for (const [chainId, safeAddresses] of Object.entries(notifiableSafes)) { + const notifiableAddedSafes = notifiableSafes[chainId] ?? [] + const uniqueSafeAddresses = Array.from(new Set([...notifiableAddedSafes, ...safeAddresses])) + + notifiableSafes[chainId] = uniqueSafeAddresses + } + + return notifiableSafes +} + +export const GlobalNotifications = (): ReactElement | null => { + const chains = useChains() + const addedSafes = useAppSelector(selectAllAddedSafes) + + const { getAllPreferences } = useNotificationPreferences() + const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() + + // Current Safes registered for notifications in IndexedDB + const currentSubscriptions = useMemo(() => { + const allPreferences = getAllPreferences() + return allPreferences ? getCurrentNotifiedSafes(allPreferences) : undefined + }, [getAllPreferences]) + + // Safes selected in the UI + const [selectedSafes, setSelectedSafes] = useState({}) + + useEffect(() => { + // `currentSubscriptions` is initially undefined until indexedDB resolves + if (currentSubscriptions) { + setSelectedSafes(currentSubscriptions) } + }, [currentSubscriptions]) - return registerable - }, [addedSafes, locallyRegisteredSafes]) + // Merged added Safes and `currentSubscriptions` (in case subscriptions aren't added) + const notifiableSafes = useMemo(() => { + return mergeNotifiableSafes(addedSafes, currentSubscriptions) + }, [addedSafes, currentSubscriptions]) const totalNotifiableSafes = useMemo(() => { return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { @@ -72,7 +110,7 @@ export const AllSafesNotifications = (): ReactElement | null => { const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { const hasChain = Object.keys(selectedSafes).includes(chainId) - const hasEverySafe = safeAddresses.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) + const hasEverySafe = safeAddresses?.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) return hasChain && hasEverySafe }) @@ -91,12 +129,16 @@ export const AllSafesNotifications = (): ReactElement | null => { }) } + // Wether Safes need to be registered or unregistered with the service const shouldRegisterSafes = Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { - return safeAddresses.some((safeAddress) => !locallyRegisteredSafes[chainId]?.includes(safeAddress)) - }) - const shouldUnregisterSafes = Object.entries(locallyRegisteredSafes).some(([chainId, safeAddresses]) => { - return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) + return safeAddresses.some((safeAddress) => !currentSubscriptions?.[chainId]?.includes(safeAddress)) }) + const shouldUnregisterSafes = + currentSubscriptions && + Object.entries(currentSubscriptions).some(([chainId, safeAddresses]) => { + return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) + }) + const canSave = shouldRegisterSafes || shouldUnregisterSafes const onSave = async () => { @@ -110,71 +152,54 @@ export const AllSafesNotifications = (): ReactElement | null => { return } - const shouldUnregisterDevice = Object.values(selectedSafes).every((safeAddresses) => safeAddresses.length === 0) + // TODO: Enable when live on prod. + // const shouldUnregisterDevice = Object.values(selectedSafes).every((safeAddresses) => safeAddresses.length === 0) + // if (shouldUnregisterDevice) { + // // Device unregister is chain agnostic + // await unregisterDevice('1', uuid) + // clearPreferences() + // return + // } - if (shouldUnregisterDevice) { - // Device unregister is chain agnostic - await unregisterDevice('1', deviceUuid) + const registrationPromises = [] - clearLocallyRegisteredSafes() - return - } + const safesToRegister = Object.entries(selectedSafes).reduce((acc, [chainId, safeAddresses]) => { + const safesToRegisterOnChain = safeAddresses.filter( + (safeAddress) => !currentSubscriptions?.[chainId]?.includes(safeAddress), + ) - const registrationPromises = [] + if (safesToRegisterOnChain.length > 0) { + acc[chainId] = safeAddresses + } - const safesToRegister: { [chainId: string]: Array } = {} - - for (const [chainId, safeAddresses] of Object.entries(selectedSafes)) { - for (const safeAddress of safeAddresses) { - const shouldUnregister = locallyRegisteredSafes[chainId]?.includes(safeAddress) - - if (shouldUnregister) { - registrationPromises.push( - unregisterSafeNotifications({ - chainId, - safeAddress: safeAddress, - deviceUuid, - callback: () => unregisterSafeLocally(chainId, safeAddress), - }), - ) - continue - } + return acc + }, {}) - // Safes to register - if (!safesToRegister[chainId]) { - safesToRegister[chainId] = [] - } + const safesToUnregister = + currentSubscriptions && + Object.entries(currentSubscriptions).reduce((acc, [chainId, safeAddresses]) => { + const safesToUnregisterOnChain = safeAddresses.filter( + (safeAddress) => !selectedSafes[chainId]?.includes(safeAddress), + ) - safesToRegister[chainId].push(safeAddress) - } - } + if (safesToUnregisterOnChain.length > 0) { + acc[chainId] = safeAddresses + } + return acc + }, {}) - const shouldRegisterSafes = Object.keys(safesToRegister).length > 0 + const shouldRegisterSafes = Object.values(safesToRegister).some((safeAddresses) => safeAddresses.length > 0) if (shouldRegisterSafes) { - const callback = () => { - Object.entries(safesToRegister).forEach(([chainId, safeAddresses]) => { - safeAddresses.forEach((safeAddress) => { - registerSafeLocally(chainId, safeAddress) - }) - }) - - dispatch( - showNotification({ - message: 'You will now receive notifications for these Safe Accounts in your browser.', - variant: 'success', - groupKey: 'notifications', - }), - ) - } + registrationPromises.push(registerNotifications(safesToRegister)) + } - registrationPromises.push( - registerNotificationDevice({ - safesToRegister, - deviceUuid, - callback, - }), - ) + if (safesToUnregister) { + for (const [chainId, safeAddresses] of Object.entries(safesToUnregister)) { + for (const safeAddress of safeAddresses) { + registrationPromises.push(unregisterSafeNotifications(chainId, safeAddress)) + } + } } Promise.all(registrationPromises) @@ -224,16 +249,9 @@ export const AllSafesNotifications = (): ReactElement | null => { const onSelectChain = () => { setSelectedSafes((prev) => { - if (isChainSelected) { - return { - ...prev, - [chainId]: [], - } - } - return { ...prev, - [chainId]: safeAddresses, + [chainId]: isChainSelected ? [] : safeAddresses, } }) } @@ -251,35 +269,31 @@ export const AllSafesNotifications = (): ReactElement | null => { - {safeAddresses.map((address) => { - const isSafeSelected = selectedSafes[chainId]?.includes(address) ?? false + {safeAddresses.map((safeAddress) => { + const isSafeSelected = selectedSafes[chainId]?.includes(safeAddress) ?? false const onSelectSafe = () => { setSelectedSafes((prev) => { - if (isSafeSelected) { - return { - ...prev, - [chainId]: prev[chainId].filter((addr) => !sameAddress(addr, address)), - } - } - return { ...prev, - [chainId]: [...(prev[chainId] ?? []), address], + [chainId]: isSafeSelected + ? prev[chainId]?.filter((addr) => !sameAddress(addr, safeAddress)) + : [...(prev[chainId] ?? []), safeAddress], } }) } return ( - + diff --git a/src/components/settings/Notifications/NotificationBanner/index.tsx b/src/components/settings/Notifications/NotificationBanner/index.tsx new file mode 100644 index 0000000000..2342e12d0d --- /dev/null +++ b/src/components/settings/Notifications/NotificationBanner/index.tsx @@ -0,0 +1,90 @@ +import { Button, Chip, SvgIcon, Typography } from '@mui/material' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useCallback, useEffect } from 'react' +import type { ReactElement } from 'react' + +import { CustomTooltip } from '@/components/common/CustomTooltip' +import { AppRoutes } from '@/config/routes' +import { useAppDispatch, useAppSelector } from '@/store' +import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' +import PushNotificationIcon from '@/public/images/notifications/push-notification.svg' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { useNotificationRegistrations } from '../hooks/useNotificationRegistrations' +import { getNotiableAddedSafes } from '../GlobalNotifications' + +import css from './styles.module.css' + +const LS_KEY = 'dismissPushNotifications' + +export const NotificationBanner = ({ children }: { children: ReactElement }): ReactElement => { + const [dismissedBanner = false, setDismissedBanner] = useLocalStorage(LS_KEY) + const dispatch = useAppDispatch() + const addedSafes = useAppSelector(selectAllAddedSafes) + const totalAddedSafes = useAppSelector(selectTotalAdded) + const { query } = useRouter() + + const { registerNotifications: registerDevice } = useNotificationRegistrations() + + const dismissBanner = useCallback(() => { + setDismissedBanner(true) + }, [setDismissedBanner]) + + // Click outside to dismiss banner + useEffect(() => { + document.addEventListener('click', dismissBanner) + return () => { + document.removeEventListener('click', dismissBanner) + } + }, [dismissBanner]) + + const onEnableAll = () => { + const safesToRegister = getNotiableAddedSafes(addedSafes) + registerDevice(safesToRegister) + + setDismissedBanner(true) + } + + if (dismissedBanner) { + return children + } + + return ( + +
+
+
+ + + Enable push notifications + +
+ + Easily track your Safe Account activity with broswer push notifications. + +
+ +
+
+ {totalAddedSafes > 0 && ( + + )} + + + +
+
+ } + open + > + {children} + + ) +} diff --git a/src/components/settings/Notifications/NotificationBanner/styles.module.css b/src/components/settings/Notifications/NotificationBanner/styles.module.css new file mode 100644 index 0000000000..575c1463d3 --- /dev/null +++ b/src/components/settings/Notifications/NotificationBanner/styles.module.css @@ -0,0 +1,57 @@ +.banner :global .MuiTooltip-tooltip { + min-width: 384px !important; + padding: var(--space-3); + border-color: var(--color-secondary-main); +} + +.banner :global .MuiTooltip-arrow::before { + border-color: var(--color-secondary-main); +} + +.container { + min-width: 100%; +} + +.content { + display: flex; + align-items: center; +} + +.text { + display: flex; + flex-direction: column; +} + +.title { + display: flex; + align-items: center; +} + +.chip { + border-radius: 4px; + background-color: var(--color-secondary-main); + margin-right: var(--space-1); + font-weight: 400; + font-size: 12px; + width: var(--space-5); + height: 24px; +} + +.chip :global .MuiChip-label { + padding: 0; + text-overflow: unset; +} + +.icon { + width: 64px; + height: 64px; +} + +.buttons { + display: flex; + gap: var(--space-2); +} + +.button { + padding: 4px 10px; +} diff --git a/src/components/settings/Notifications/hooks/notifications-idb.ts b/src/components/settings/Notifications/hooks/notifications-idb.ts new file mode 100644 index 0000000000..6cd21f44e6 --- /dev/null +++ b/src/components/settings/Notifications/hooks/notifications-idb.ts @@ -0,0 +1,31 @@ +import { createStore } from 'idb-keyval' + +import type { WebhookType } from '@/services/firebase/webhooks' + +export const createUuidStore = () => { + const DB_NAME = 'notifications-uuid-database' + const STORE_NAME = 'notifications-uuid-store' + + return createStore(DB_NAME, STORE_NAME) +} + +export type SafeNotificationKey = `${string}:${string}` + +export type NotificationPreferences = { + [safeKey: SafeNotificationKey]: { + chainId: string + safeAddress: string + preferences: { [key in WebhookType]: boolean } + } +} + +export const getSafeNotificationKey = (chainId: string, safeAddress: string): SafeNotificationKey => { + return `${chainId}:${safeAddress}` +} + +export const createPreferencesStore = () => { + const DB_NAME = 'notifications-preferences-database' + const STORE_NAME = 'notifications-preferences-store' + + return createStore(DB_NAME, STORE_NAME) +} diff --git a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts new file mode 100644 index 0000000000..0f08ec36d6 --- /dev/null +++ b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts @@ -0,0 +1,181 @@ +import { get, set, entries, delMany, setMany, clear } from 'idb-keyval' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import { WebhookType } from '@/services/firebase/webhooks' +import { createPreferencesStore, createUuidStore, getSafeNotificationKey } from './notifications-idb' +import type { NotificationPreferences, SafeNotificationKey } from './notifications-idb' + +const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationKey]['preferences'] = { + [WebhookType.NEW_CONFIRMATION]: true, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, + [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, + [WebhookType.INCOMING_ETHER]: true, + [WebhookType.OUTGOING_ETHER]: true, + [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 +} + +// TODO: mounted check in effects + +export const useNotificationPreferences = () => { + const [uuid, setUuid] = useState('') + const [preferences, setPreferences] = useState() + + // UUID store + const uuidStore = useMemo(() => { + if (typeof window !== 'undefined') { + return createUuidStore() + } + }, []) + + // Load/initialise UUID + useEffect(() => { + if (!uuidStore) { + return + } + + const UUID_KEY = 'uuid' + + get(UUID_KEY, uuidStore) + .then((uuid) => { + if (!uuid) { + uuid = self.crypto.randomUUID() + set(UUID_KEY, uuid, uuidStore) + } + + setUuid(uuid) + }) + .catch(() => null) + }, [uuidStore]) + + // Preferences store + const preferencesStore = useMemo(() => { + if (typeof window !== 'undefined') { + return createPreferencesStore() + } + }, []) + + // Load preferences + useEffect(() => { + if (!preferencesStore) { + return + } + + entries(preferencesStore) + .then((preferencesEntries) => { + setPreferences(Object.fromEntries(preferencesEntries)) + }) + .catch(() => null) + }, [preferencesStore]) + + const getPreferences = (chainId: string, safeAddress: string) => { + const key = getSafeNotificationKey(chainId, safeAddress) + return preferences?.[key]?.preferences + } + + const getAllPreferences = useCallback(() => { + return preferences + }, [preferences]) + + const createPreferences = (safesToRegister: { [chain: string]: Array }) => { + if (!preferencesStore) { + return + } + + const defaultPreferencesEntries = Object.entries(safesToRegister).flatMap(([chainId, safeAddresses]) => { + return safeAddresses.map((safeAddress): [SafeNotificationKey, NotificationPreferences[SafeNotificationKey]] => { + const key = getSafeNotificationKey(chainId, safeAddress) + + return [ + key, + { + chainId, + safeAddress, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + }, + ] + }) + }) + + setMany(defaultPreferencesEntries, preferencesStore) + .then(() => { + setPreferences(Object.fromEntries(defaultPreferencesEntries)) + }) + .catch(() => null) + } + + const updatePreferences = ( + chainId: string, + safeAddress: string, + preferences: NotificationPreferences[SafeNotificationKey]['preferences'], + ) => { + if (!preferencesStore) { + return + } + + const key = getSafeNotificationKey(chainId, safeAddress) + + set(key, preferences, preferencesStore) + .then(() => { + setPreferences((prev) => ({ + ...prev, + [key]: { + ...prev?.[key], + preferences, + }, + })) + }) + .catch(() => null) + } + + const deletePreferences = (safesToUnregister: { [chain: string]: Array }) => { + if (!preferencesStore) { + return + } + + const keysToDelete = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { + return safeAddresses.map((safeAddress) => getSafeNotificationKey(chainId, safeAddress)) + }) + + delMany(keysToDelete, preferencesStore) + .then(() => { + setPreferences((prev) => { + if (!prev) { + return + } + + const newEntries = Object.entries(prev).filter(([key]) => { + return !keysToDelete.includes(key as SafeNotificationKey) + }) + + return Object.fromEntries(newEntries) + }) + }) + .catch(() => null) + } + + const clearPreferences = () => { + if (!preferencesStore) { + return + } + + clear(preferencesStore) + .then(() => { + setPreferences({}) + }) + .catch(() => null) + } + + return { + uuid, + getAllPreferences, + getPreferences, + updatePreferences, + _createPreferences: createPreferences, + _deletePreferences: deletePreferences, + _clearPreferences: clearPreferences, + } +} diff --git a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts new file mode 100644 index 0000000000..a7a27c3e1d --- /dev/null +++ b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts @@ -0,0 +1,88 @@ +import { registerDevice, unregisterDevice, unregisterSafe } from '@safe-global/safe-gateway-typescript-sdk' + +import { useWeb3 } from '@/hooks/wallets/web3' +import { useAppDispatch } from '@/store' +import { showNotification } from '@/store/notificationsSlice' +import { getRegisterDevicePayload } from '../logic' +import { useNotificationPreferences } from './useNotificationPreferences' + +type Subscriptions = { [chainId: string]: Array } + +export const useNotificationRegistrations = () => { + const dispatch = useAppDispatch() + const web3 = useWeb3() + + const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences() + + const registerNotifications = async (safesToRegister: Subscriptions, withSignature = false) => { + let didRegister = false + + try { + const payload = await getRegisterDevicePayload({ + uuid, + safesToRegister, + web3: withSignature ? web3 : undefined, + }) + + // 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) + } + + if (!didRegister) { + return + } + + _createPreferences(safesToRegister) + + if (!withSignature) { + const isMultiple = + Object.keys(safesToRegister).length > 1 || Object.values(safesToRegister).some((safes) => safes.length > 1) + + dispatch( + showNotification({ + message: `You will now receive notifications for ${ + isMultiple ? 'these Safe Accounts' : 'this Safe Account' + } in your browser.`, + variant: 'success', + groupKey: 'notifications', + }), + ) + } + } + + const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { + let didUnregister = false + + try { + const response = await unregisterSafe(chainId, safeAddress, uuid) + + didUnregister = response == null + } catch (e) { + console.error(`Error unregistering ${safeAddress} on chain ${chainId}`, e) + } + + if (didUnregister) { + _deletePreferences({ [chainId]: [safeAddress] }) + } + } + + const unregisterAllNotifications = () => { + // Device unregistration is chain agnostic + unregisterDevice('1', uuid) + .then(() => { + _clearPreferences() + }) + .catch(() => null) + } + + return { + registerNotifications, + unregisterSafeNotifications, + unregisterAllNotifications, + } +} diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index b609a97051..0a400802e1 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -1,74 +1,43 @@ -import { - Grid, - Paper, - Typography, - Checkbox, - FormControlLabel, - FormGroup, - Alert, - Switch, - Divider, - AlertTitle, -} from '@mui/material' +import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch, Divider } from '@mui/material' import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' -import CheckWallet from '@/components/common/CheckWallet' import EthHashInfo from '@/components/common/EthHashInfo' import { WebhookType } from '@/services/firebase/webhooks' -import { useNotificationDb } from './useNotificationDb' -import { AllSafesNotifications } from './AllSafesNotifications' +import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' +import { useNotificationPreferences } from './hooks/useNotificationPreferences' +import { GlobalNotifications } from './GlobalNotifications' import useIsSafeOwner from '@/hooks/useIsSafeOwner' -import { registerNotificationDevice, unregisterSafeNotifications } from './logic' -import { useWeb3 } from '@/hooks/wallets/web3' import { IS_DEV } from '@/config/constants' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' import css from './styles.module.css' -export const Notifications = (): ReactElement => { - const web3 = useWeb3() +export const SafeNotifications = (): ReactElement => { const dispatch = useAppDispatch() - const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() - const { - deviceUuid, - isSafeRegistered, - notificationPreferences, - setNotificationPreferences, - registerSafeLocally, - unregisterSafeLocally, - } = useNotificationDb() + const { getPreferences, updatePreferences } = useNotificationPreferences() + const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() + + const preferences = getPreferences(safe.chainId, safe.address.value) + + const setPreferences = (newPreferences: NonNullable>) => { + updatePreferences(safe.chainId, safe.address.value, newPreferences) + } + + const isSafeRegistered = !!preferences const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') + const shouldShowMacHelper = isMac || IS_DEV const handleOnChange = () => { if (isSafeRegistered) { - unregisterSafeNotifications({ - deviceUuid, - chainId: safe.chainId, - safeAddress: safe.address.value, - callback: () => unregisterSafeLocally(safe.chainId, safe.address.value), - }) + unregisterSafeNotifications(safe.chainId, safe.address.value) } else { - registerNotificationDevice({ - deviceUuid, - safesToRegister: { [safe.chainId]: [safe.address.value] }, - callback: () => { - registerSafeLocally(safe.chainId, safe.address.value) - - dispatch( - showNotification({ - message: 'You will now receive notifications for this Safe Account in your browser.', - variant: 'success', - groupKey: 'notification', - }), - ) - }, - }) + registerNotifications({ [safe.chainId]: [safe.address.value] }) } } @@ -89,11 +58,15 @@ export const Notifications = (): ReactElement => { You will need to enable them again if you clear your browser cache. - {(isMac || IS_DEV) && ( + {shouldShowMacHelper && ( - For MacOS users - Double-check that you have enabled your browser notifications under System Settings >{' '} - Notifications > Application Notifications (path may vary depending on OS version). + + For MacOS users + + + Double-check that you have enabled your browser notifications under System Settings >{' '} + Notifications > Application Notifications (path may vary depending on OS version). + )} @@ -109,31 +82,20 @@ export const Notifications = (): ReactElement => { showName={true} hasExplorer /> - - {(isOk) => ( - - } - label={!!isSafeRegistered ? 'On' : 'Off'} - /> - )} - + } + label={!!isSafeRegistered ? 'On' : 'Off'} + /> ) : ( - + )}
- {safeLoaded && ( + {preferences && ( @@ -147,18 +109,14 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.INCOMING_ETHER]: checked, [WebhookType.INCOMING_TOKEN]: checked, }) }} - disabled={!isSafeRegistered} /> } label="Incoming assets" @@ -167,18 +125,14 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.OUTGOING_ETHER]: checked, [WebhookType.OUTGOING_TOKEN]: checked, }) }} - disabled={!isSafeRegistered} /> } label="Outgoing assets" @@ -186,14 +140,13 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, }) }} - disabled={!isSafeRegistered} /> } label="Pending transactions" @@ -202,14 +155,13 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.NEW_CONFIRMATION]: checked, }) }} - disabled={!isSafeRegistered} /> } label="New confirmations" @@ -218,14 +170,13 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, }) }} - disabled={!isSafeRegistered} /> } label="Executed transactions" @@ -234,14 +185,13 @@ export const Notifications = (): ReactElement => { { - setNotificationPreferences({ - ...notificationPreferences, + setPreferences({ + ...preferences, [WebhookType.MODULE_TRANSACTION]: checked, }) }} - disabled={!isSafeRegistered} /> } label="Module transactions" @@ -250,20 +200,30 @@ export const Notifications = (): ReactElement => { { - registerNotificationDevice({ - deviceUuid, - safesToRegister: { + checked={preferences?.[WebhookType.CONFIRMATION_REQUEST]} + onChange={async (_, checked) => { + registerNotifications( + { [safe.chainId]: [safe.address.value], }, - web3, // Add signature - callback: () => - setNotificationPreferences({ - ...notificationPreferences, + true, // Add signature + ) + .then(() => { + setPreferences({ + ...preferences, [WebhookType.CONFIRMATION_REQUEST]: checked, - }), - }) + }) + + dispatch( + showNotification({ + message: + 'You will now receive notifications about confirmation requests for this Safe Account in your browser.', + variant: 'success', + groupKey: 'notifications', + }), + ) + }) + .catch(() => null) }} /> } diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts index a606ed431d..7bfb24cd46 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/Notifications/logic.ts @@ -1,6 +1,5 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { getToken, getMessaging } from 'firebase/messaging' -import { registerDevice, unregisterSafe } from '@safe-global/safe-gateway-typescript-sdk' import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' @@ -56,27 +55,13 @@ const getSafeRegistrationSignature = ({ return web3.getSigner().signMessage(hashedMessage) } -type RegisterDeviceParams = - | { - safesToRegister: { [chainId: string]: Array } - deviceUuid: string - callback: () => void - web3?: never - } - | { - safesToRegister: { [chainId: string]: Array } - deviceUuid: string - callback?: () => void - web3: Web3Provider - } - export const getRegisterDevicePayload = async ({ safesToRegister, - deviceUuid, + uuid, web3, }: { safesToRegister: { [chainId: string]: Array } - deviceUuid: string + uuid: string web3?: Web3Provider }): Promise => { const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) @@ -103,7 +88,7 @@ export const getRegisterDevicePayload = async ({ Object.entries(safesToRegister).map(async ([chainId, safeAddresses]) => { // Signature is only required for CONFIRMATION_REQUESTS const signature = web3 - ? await getSafeRegistrationSignature({ safes: safeAddresses, web3, deviceUuid, timestamp, token }) + ? await getSafeRegistrationSignature({ safes: safeAddresses, web3, deviceUuid: uuid, timestamp, token }) : undefined return { @@ -115,7 +100,7 @@ export const getRegisterDevicePayload = async ({ ) return { - uuid: deviceUuid, + uuid: uuid, cloudMessagingToken: token, buildNumber: '0', // Required value, but does not exist on web bundle: location.origin, @@ -125,54 +110,3 @@ export const getRegisterDevicePayload = async ({ safeRegistrations, } } - -export const registerNotificationDevice = async ({ - safesToRegister, - deviceUuid, - callback, - web3, -}: RegisterDeviceParams) => { - let didRegister = false - - try { - const payload = await getRegisterDevicePayload({ deviceUuid, safesToRegister, 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) - } - - if (didRegister) { - callback?.() - } -} - -export const unregisterSafeNotifications = async ({ - chainId, - safeAddress, - deviceUuid, - callback, -}: { - chainId: string - safeAddress: string - deviceUuid: string - callback: () => void -}) => { - let didUnregister = false - - try { - const response = await unregisterSafe(chainId, safeAddress, deviceUuid) - - didUnregister = response == null - } catch (e) { - console.error(`Error unregistering ${safeAddress} on chain ${chainId}`, e) - } - - if (didUnregister) { - callback() - } -} diff --git a/src/components/settings/Notifications/styles.module.css b/src/components/settings/Notifications/styles.module.css index eee7725502..1a42be0c9a 100644 --- a/src/components/settings/Notifications/styles.module.css +++ b/src/components/settings/Notifications/styles.module.css @@ -13,14 +13,6 @@ padding: 0; } -/* TODO: Move to theme */ -.switch :global .MuiSwitch-switchBase { - color: var(--color-primary-main); -} -.switch :global .MuiSwitch-track { - background-color: var(--color-primary-main) !important; -} - .item { padding-left: var(--space-1); } diff --git a/src/components/settings/Notifications/useNotificationDb.ts b/src/components/settings/Notifications/useNotificationDb.ts deleted file mode 100644 index 1875c7519f..0000000000 --- a/src/components/settings/Notifications/useNotificationDb.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import { del, delMany, entries, set, setMany } from 'idb-keyval' - -import { WebhookType } from '@/services/firebase/webhooks' -import useSafeInfo from '@/hooks/useSafeInfo' -import { getNotificationDbKey, createNotificationDbStore } from '@/services/firebase/notification-db' -import type { NotificationDbKey } from '@/services/firebase/notification-db' - -const UUID_KEY = '__uuid' - -const defaultNotificationPreferences = { - [WebhookType.NEW_CONFIRMATION]: true, - [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, - [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, - [WebhookType.INCOMING_ETHER]: true, - [WebhookType.OUTGOING_ETHER]: true, - [WebhookType.INCOMING_TOKEN]: true, - [WebhookType.OUTGOING_TOKEN]: true, - [WebhookType.MODULE_TRANSACTION]: true, - [WebhookType.CONFIRMATION_REQUEST]: false, // Requires signature -} - -export const useNotificationDb = () => { - const { safe } = useSafeInfo() - - const SAFE_STORE_NAME = - safe.chainId && safe.address.value ? getNotificationDbKey(safe.chainId, safe.address.value) : undefined - - const [notificationDb, setNotificationDb] = useState< - Record & { [UUID_KEY]: string } - >({ - [UUID_KEY]: '', - }) - - const store = useMemo(() => { - if (typeof window !== 'undefined') { - return createNotificationDbStore() - } - }, []) - - // Load database - useEffect(() => { - if (!store) { - return - } - - entries(store) - .then(async (entries) => { - const db = Object.fromEntries(entries) - - // Set UUID if it does not exist - if (!db[UUID_KEY]) { - const uuid = self.crypto.randomUUID() - db[UUID_KEY] = uuid - - set(UUID_KEY, uuid, store) - } - - setNotificationDb((prev) => ({ ...prev, ...db })) - }) - .catch(() => null) - }, [store]) - - const notificationPreferences = SAFE_STORE_NAME ? notificationDb[SAFE_STORE_NAME] : undefined - - const setNotificationPreferences = (preferences: typeof defaultNotificationPreferences) => { - if (!SAFE_STORE_NAME) { - return - } - - const db = { - ...notificationDb, - [SAFE_STORE_NAME]: { - ...notificationPreferences, - preferences, - }, - } - - setMany(Object.entries(db), store) - .then(() => { - setNotificationDb((prev) => ({ ...prev, [SAFE_STORE_NAME]: preferences })) - }) - .catch(() => null) - } - - const locallyRegisteredSafes = useMemo(() => { - const safes: { [chainId: string]: Array } = {} - - for (const key of Object.keys(notificationDb)) { - if (key === UUID_KEY) { - continue - } - - const [chainId, address] = key.split(':') - - if (!safes[chainId]) { - safes[chainId] = [] - } - - safes[chainId].push(address) - } - - return safes - }, [notificationDb]) - - const registerSafeLocally = (chainId: string, safeAddress: string) => { - const key = getNotificationDbKey(chainId, safeAddress) - - set(key, defaultNotificationPreferences, store) - .then(() => { - setNotificationDb((prev) => ({ ...prev, [key]: defaultNotificationPreferences })) - }) - .catch(() => null) - } - - const unregisterSafeLocally = (chainId: string, safeAddress: string) => { - const key = getNotificationDbKey(chainId, safeAddress) - - del(key, store) - .then(() => { - setNotificationDb((prev) => { - delete prev[key] - - return prev - }) - }) - .catch(() => null) - } - - const clearLocallyRegisteredSafes = () => { - const keys = Object.keys(notificationDb).filter((key) => key !== UUID_KEY) - - delMany(keys, store) - .then(() => { - setNotificationDb(({ [UUID_KEY]: uuid }) => { - return { [UUID_KEY]: uuid } - }) - }) - .catch(() => null) - } - - return { - deviceUuid: notificationDb[UUID_KEY], - notificationPreferences: notificationPreferences ?? defaultNotificationPreferences, - setNotificationPreferences, - isSafeRegistered: !!notificationPreferences, - locallyRegisteredSafes, - registerSafeLocally, - unregisterSafeLocally, - clearLocallyRegisteredSafes, - } -} diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx index 0773776402..6793caab24 100644 --- a/src/pages/settings/notifications.tsx +++ b/src/pages/settings/notifications.tsx @@ -2,7 +2,7 @@ import Head from 'next/head' import type { NextPage } from 'next' import SettingsHeader from '@/components/settings/SettingsHeader' -import { Notifications } from '@/components/settings/Notifications' +import { SafeNotifications } from '@/components/settings/Notifications' const NotificationsPage: NextPage = () => { return ( @@ -14,7 +14,7 @@ const NotificationsPage: NextPage = () => {
- +
) diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index f088a7c125..927e28dcc5 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -8,7 +8,11 @@ import { AppRoutes } from '@/config/routes' import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' import type { WebhookEvent } from '@/services/firebase/webhooks' import { GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' -import { createNotificationDbStore, getNotificationDbKey } from './notification-db' +import { + createPreferencesStore, + getSafeNotificationKey, +} from '@/components/settings/Notifications/hooks/notifications-idb' +import type { NotificationPreferences } from '@/components/settings/Notifications/hooks/notifications-idb' export const shouldShowNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { @@ -17,12 +21,17 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise(chainId, store).catch(() => null) - const safeStore = await get(key, store).catch(() => null) + if (!preferencesStore) { + return false + } + + const key = getSafeNotificationKey(chainId, address) + const notificationPreferences = preferencesStore[key]?.preferences - return safeStore?.[type] ?? true + return !!notificationPreferences[type] } // localStorage cannot be accessed in service workers so we reference the flag diff --git a/src/services/firebase/notification-db.ts b/src/services/firebase/notification-db.ts deleted file mode 100644 index 9f524449d6..0000000000 --- a/src/services/firebase/notification-db.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createStore } from 'idb-keyval' - -export type NotificationDbKey = `${string}:${string}` - -export const getNotificationDbKey = (chainId: string, safeAddress: string): NotificationDbKey => { - return `${chainId}:${safeAddress}` -} - -export const createNotificationDbStore = () => { - const DB_NAME = 'notifications-database' - const STORE_NAME = 'notifications-store' - - return createStore(DB_NAME, STORE_NAME) -} From c770a399881f28c34190a1b2daf9a2b2b6b6c339 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 24 Aug 2023 22:21:13 +0200 Subject: [PATCH 19/62] fix: database reactivity with `ExternalStore` --- .../Notifications/GlobalNotifications.tsx | 87 ++++++----- .../NotificationBanner/index.tsx | 31 ++-- .../hooks/useNotificationPreferences.ts | 144 +++++++++--------- .../hooks/useNotificationRegistrations.ts | 12 ++ .../settings/Notifications/index.tsx | 26 ++-- 5 files changed, 165 insertions(+), 135 deletions(-) diff --git a/src/components/settings/Notifications/GlobalNotifications.tsx b/src/components/settings/Notifications/GlobalNotifications.tsx index 00cd984738..307e9d347b 100644 --- a/src/components/settings/Notifications/GlobalNotifications.tsx +++ b/src/components/settings/Notifications/GlobalNotifications.tsx @@ -31,42 +31,43 @@ import css from './styles.module.css' type NotifiableSafes = { [chainId: string]: Array } // Convert data structure of added Safes -export const getNotiableAddedSafes = (addedSafes: AddedSafesState): NotifiableSafes => { - const notifiableAddedSafes: NotifiableSafes = {} +export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafes => { + const obj: NotifiableSafes = {} for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { - notifiableAddedSafes[chainId] = Object.keys(addedSafesOnChain) + obj[chainId] = Object.keys(addedSafesOnChain) } - return notifiableAddedSafes + return obj } -// Convert data structure of locally notification-registered Safes -const getCurrentNotifiedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { - const currentNotifiedSafes: NotifiableSafes = {} +// Convert data structure of currently notified Safes +const transformCurrentNotifiedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { + const obj: NotifiableSafes = {} for (const { chainId, safeAddress } of Object.values(allPreferences)) { - if (!currentNotifiedSafes[chainId]) { - currentNotifiedSafes[chainId] = [] + if (!obj[chainId]) { + obj[chainId] = [] } - currentNotifiedSafes[chainId].push(safeAddress) + obj[chainId].push(safeAddress) } - return currentNotifiedSafes + return obj } +// Merges added Safes and currently notified Safes into a single data structure without duplicates const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions?: NotifiableSafes): NotifiableSafes => { - const notifiableSafes = getNotiableAddedSafes(addedSafes) + const notifiableSafes = transformAddedSafes(addedSafes) if (!currentSubscriptions) { return notifiableSafes } // Locally registered Safes (if not already added) - for (const [chainId, safeAddresses] of Object.entries(notifiableSafes)) { - const notifiableAddedSafes = notifiableSafes[chainId] ?? [] - const uniqueSafeAddresses = Array.from(new Set([...notifiableAddedSafes, ...safeAddresses])) + for (const [chainId, safeAddresses] of Object.entries(currentSubscriptions)) { + const notifiableSafesOnChain = notifiableSafes[chainId] ?? [] + const uniqueSafeAddresses = Array.from(new Set([...notifiableSafesOnChain, ...safeAddresses])) notifiableSafes[chainId] = uniqueSafeAddresses } @@ -81,26 +82,35 @@ export const GlobalNotifications = (): ReactElement | null => { const { getAllPreferences } = useNotificationPreferences() const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() - // Current Safes registered for notifications in IndexedDB - const currentSubscriptions = useMemo(() => { - const allPreferences = getAllPreferences() - return allPreferences ? getCurrentNotifiedSafes(allPreferences) : undefined - }, [getAllPreferences]) - // Safes selected in the UI const [selectedSafes, setSelectedSafes] = useState({}) + // Current Safes registered for notifications in indexedDB + const currentNotifiedSafes = useMemo(() => { + const allPreferences = getAllPreferences() + if (!allPreferences) { + return + } + return transformCurrentNotifiedSafes(allPreferences) + }, [getAllPreferences]) + + // `currentNotifiedSafes` is initially undefined until indexedDB resolves useEffect(() => { - // `currentSubscriptions` is initially undefined until indexedDB resolves - if (currentSubscriptions) { - setSelectedSafes(currentSubscriptions) + let isMounted = true + + if (currentNotifiedSafes && isMounted) { + setSelectedSafes(currentNotifiedSafes) } - }, [currentSubscriptions]) - // Merged added Safes and `currentSubscriptions` (in case subscriptions aren't added) + return () => { + isMounted = false + } + }, [currentNotifiedSafes]) + + // Merged added Safes and `currentNotifiedSafes` (in case subscriptions aren't added) const notifiableSafes = useMemo(() => { - return mergeNotifiableSafes(addedSafes, currentSubscriptions) - }, [addedSafes, currentSubscriptions]) + return mergeNotifiableSafes(addedSafes, currentNotifiedSafes) + }, [addedSafes, currentNotifiedSafes]) const totalNotifiableSafes = useMemo(() => { return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { @@ -129,13 +139,14 @@ export const GlobalNotifications = (): ReactElement | null => { }) } - // Wether Safes need to be registered or unregistered with the service + // Wether Safes need to be (un-)reigstered with the service const shouldRegisterSafes = Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { - return safeAddresses.some((safeAddress) => !currentSubscriptions?.[chainId]?.includes(safeAddress)) + return safeAddresses.some((safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress)) }) + const shouldUnregisterSafes = - currentSubscriptions && - Object.entries(currentSubscriptions).some(([chainId, safeAddresses]) => { + currentNotifiedSafes && + Object.entries(currentNotifiedSafes).some(([chainId, safeAddresses]) => { return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) }) @@ -155,17 +166,15 @@ export const GlobalNotifications = (): ReactElement | null => { // TODO: Enable when live on prod. // const shouldUnregisterDevice = Object.values(selectedSafes).every((safeAddresses) => safeAddresses.length === 0) // if (shouldUnregisterDevice) { - // // Device unregister is chain agnostic - // await unregisterDevice('1', uuid) - // clearPreferences() + // unregisterAllNotifications() // return // } - const registrationPromises = [] + const registrationPromises: Array> = [] const safesToRegister = Object.entries(selectedSafes).reduce((acc, [chainId, safeAddresses]) => { const safesToRegisterOnChain = safeAddresses.filter( - (safeAddress) => !currentSubscriptions?.[chainId]?.includes(safeAddress), + (safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress), ) if (safesToRegisterOnChain.length > 0) { @@ -176,8 +185,8 @@ export const GlobalNotifications = (): ReactElement | null => { }, {}) const safesToUnregister = - currentSubscriptions && - Object.entries(currentSubscriptions).reduce((acc, [chainId, safeAddresses]) => { + currentNotifiedSafes && + Object.entries(currentNotifiedSafes).reduce((acc, [chainId, safeAddresses]) => { const safesToUnregisterOnChain = safeAddresses.filter( (safeAddress) => !selectedSafes[chainId]?.includes(safeAddress), ) diff --git a/src/components/settings/Notifications/NotificationBanner/index.tsx b/src/components/settings/Notifications/NotificationBanner/index.tsx index 2342e12d0d..fe47fc84f8 100644 --- a/src/components/settings/Notifications/NotificationBanner/index.tsx +++ b/src/components/settings/Notifications/NotificationBanner/index.tsx @@ -6,12 +6,12 @@ import type { ReactElement } from 'react' import { CustomTooltip } from '@/components/common/CustomTooltip' import { AppRoutes } from '@/config/routes' -import { useAppDispatch, useAppSelector } from '@/store' +import { useAppSelector } from '@/store' import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' import PushNotificationIcon from '@/public/images/notifications/push-notification.svg' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useNotificationRegistrations } from '../hooks/useNotificationRegistrations' -import { getNotiableAddedSafes } from '../GlobalNotifications' +import { transformAddedSafes } from '../GlobalNotifications' import css from './styles.module.css' @@ -19,12 +19,13 @@ const LS_KEY = 'dismissPushNotifications' export const NotificationBanner = ({ children }: { children: ReactElement }): ReactElement => { const [dismissedBanner = false, setDismissedBanner] = useLocalStorage(LS_KEY) - const dispatch = useAppDispatch() const addedSafes = useAppSelector(selectAllAddedSafes) const totalAddedSafes = useAppSelector(selectTotalAdded) + const { query } = useRouter() + const safe = Array.isArray(query.safe) ? query.safe[0] : query.safe - const { registerNotifications: registerDevice } = useNotificationRegistrations() + const { registerNotifications } = useNotificationRegistrations() const dismissBanner = useCallback(() => { setDismissedBanner(true) @@ -32,15 +33,19 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re // Click outside to dismiss banner useEffect(() => { + if (dismissedBanner) { + return + } + document.addEventListener('click', dismissBanner) return () => { document.removeEventListener('click', dismissBanner) } - }, [dismissBanner]) + }, [dismissBanner, dismissedBanner]) const onEnableAll = () => { - const safesToRegister = getNotiableAddedSafes(addedSafes) - registerDevice(safesToRegister) + const safesToRegister = transformAddedSafes(addedSafes) + registerNotifications(safesToRegister) setDismissedBanner(true) } @@ -74,11 +79,13 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re Enable all )} - - - + {safe && ( + + + + )} } diff --git a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts index 0f08ec36d6..1e3795211c 100644 --- a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts @@ -1,7 +1,8 @@ -import { get, set, entries, delMany, setMany, clear } from 'idb-keyval' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { set, entries, delMany, setMany, clear, update } from 'idb-keyval' +import { useCallback, useEffect, useMemo } from 'react' import { WebhookType } from '@/services/firebase/webhooks' +import ExternalStore from '@/services/ExternalStore' import { createPreferencesStore, createUuidStore, getSafeNotificationKey } from './notifications-idb' import type { NotificationPreferences, SafeNotificationKey } from './notifications-idb' @@ -18,48 +19,70 @@ const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotification [WebhookType.SAFE_CREATED]: false, // Cannot be registered to predicted address } -// TODO: mounted check in effects +// ExternalStores are used to keep indexedDB state longer than the component lifecycle +const { useStore: useUuid, setStore: setUuid } = new ExternalStore() +const { useStore: usePreferences, setStore: setPreferences } = new ExternalStore() export const useNotificationPreferences = () => { - const [uuid, setUuid] = useState('') - const [preferences, setPreferences] = useState() + // State + const uuid = useUuid() + const preferences = usePreferences() - // UUID store + // Getters + const getPreferences = (chainId: string, safeAddress: string) => { + const key = getSafeNotificationKey(chainId, safeAddress) + return preferences?.[key]?.preferences + } + + const getAllPreferences = useCallback(() => { + return preferences + }, [preferences]) + + // idb-keyval stores const uuidStore = useMemo(() => { - if (typeof window !== 'undefined') { + if (typeof indexedDB !== 'undefined') { return createUuidStore() } }, []) - // Load/initialise UUID - useEffect(() => { + const preferencesStore = useMemo(() => { + if (typeof indexedDB !== 'undefined') { + return createPreferencesStore() + } + }, []) + + // UUID state hydrator + const hydrateUuidStore = useCallback(() => { if (!uuidStore) { return } const UUID_KEY = 'uuid' - get(UUID_KEY, uuidStore) - .then((uuid) => { - if (!uuid) { - uuid = self.crypto.randomUUID() - set(UUID_KEY, uuid, uuidStore) - } - - setUuid(uuid) + let _uuid: string + + update( + UUID_KEY, + (storedUuid) => { + // Initialise UUID if it doesn't exist + _uuid = storedUuid || self.crypto.randomUUID() + return _uuid + }, + uuidStore, + ) + .then(() => { + setUuid(_uuid) }) .catch(() => null) }, [uuidStore]) - // Preferences store - const preferencesStore = useMemo(() => { - if (typeof window !== 'undefined') { - return createPreferencesStore() - } - }, []) - - // Load preferences + // Hydrate UUID state useEffect(() => { + hydrateUuidStore() + }, [hydrateUuidStore, uuidStore]) + + // Preferences state hydrator + const hydratePreferences = useCallback(() => { if (!preferencesStore) { return } @@ -71,15 +94,12 @@ export const useNotificationPreferences = () => { .catch(() => null) }, [preferencesStore]) - const getPreferences = (chainId: string, safeAddress: string) => { - const key = getSafeNotificationKey(chainId, safeAddress) - return preferences?.[key]?.preferences - } - - const getAllPreferences = useCallback(() => { - return preferences - }, [preferences]) + // Hydrate preferences state + useEffect(() => { + hydratePreferences() + }, [hydratePreferences]) + // Add store entry with default preferences for specified Safe(s) const createPreferences = (safesToRegister: { [chain: string]: Array }) => { if (!preferencesStore) { return @@ -89,24 +109,22 @@ export const useNotificationPreferences = () => { return safeAddresses.map((safeAddress): [SafeNotificationKey, NotificationPreferences[SafeNotificationKey]] => { const key = getSafeNotificationKey(chainId, safeAddress) - return [ - key, - { - chainId, - safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, - }, - ] + const defaultPreferences: NotificationPreferences[SafeNotificationKey] = { + chainId, + safeAddress, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, + } + + return [key, defaultPreferences] }) }) setMany(defaultPreferencesEntries, preferencesStore) - .then(() => { - setPreferences(Object.fromEntries(defaultPreferencesEntries)) - }) + .then(hydratePreferences) .catch(() => null) } + // Update preferences for specified Safe const updatePreferences = ( chainId: string, safeAddress: string, @@ -118,19 +136,18 @@ export const useNotificationPreferences = () => { const key = getSafeNotificationKey(chainId, safeAddress) - set(key, preferences, preferencesStore) - .then(() => { - setPreferences((prev) => ({ - ...prev, - [key]: { - ...prev?.[key], - preferences, - }, - })) - }) + const newPreferences: NotificationPreferences[SafeNotificationKey] = { + safeAddress, + chainId, + preferences, + } + + set(key, newPreferences, preferencesStore) + .then(hydratePreferences) .catch(() => null) } + // Delete preferences store entry for specified Safe(s) const deletePreferences = (safesToUnregister: { [chain: string]: Array }) => { if (!preferencesStore) { return @@ -141,31 +158,18 @@ export const useNotificationPreferences = () => { }) delMany(keysToDelete, preferencesStore) - .then(() => { - setPreferences((prev) => { - if (!prev) { - return - } - - const newEntries = Object.entries(prev).filter(([key]) => { - return !keysToDelete.includes(key as SafeNotificationKey) - }) - - return Object.fromEntries(newEntries) - }) - }) + .then(hydratePreferences) .catch(() => null) } + // Delete all preferences store entries const clearPreferences = () => { if (!preferencesStore) { return } clear(preferencesStore) - .then(() => { - setPreferences({}) - }) + .then(hydratePreferences) .catch(() => null) } diff --git a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts index a7a27c3e1d..6c15ca57aa 100644 --- a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts @@ -15,6 +15,10 @@ export const useNotificationRegistrations = () => { const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences() const registerNotifications = async (safesToRegister: Subscriptions, withSignature = false) => { + if (!uuid) { + return + } + let didRegister = false try { @@ -56,6 +60,10 @@ export const useNotificationRegistrations = () => { } const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { + if (!uuid) { + return + } + let didUnregister = false try { @@ -72,6 +80,10 @@ export const useNotificationRegistrations = () => { } const unregisterAllNotifications = () => { + if (!uuid) { + return + } + // Device unregistration is chain agnostic unregisterDevice('1', uuid) .then(() => { diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 0a400802e1..1e070f90ef 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -19,7 +19,7 @@ export const SafeNotifications = (): ReactElement => { const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() - const { getPreferences, updatePreferences } = useNotificationPreferences() + const { updatePreferences, getPreferences } = useNotificationPreferences() const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() const preferences = getPreferences(safe.chainId, safe.address.value) @@ -28,13 +28,11 @@ export const SafeNotifications = (): ReactElement => { updatePreferences(safe.chainId, safe.address.value, newPreferences) } - const isSafeRegistered = !!preferences - const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') const shouldShowMacHelper = isMac || IS_DEV const handleOnChange = () => { - if (isSafeRegistered) { + if (preferences) { unregisterSafeNotifications(safe.chainId, safe.address.value) } else { registerNotifications({ [safe.chainId]: [safe.address.value] }) @@ -83,8 +81,8 @@ export const SafeNotifications = (): ReactElement => { hasExplorer /> } - label={!!isSafeRegistered ? 'On' : 'Off'} + control={} + label={preferences ? 'On' : 'Off'} /> @@ -109,7 +107,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -125,7 +123,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -140,7 +138,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -155,7 +153,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -170,7 +168,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -185,7 +183,7 @@ export const SafeNotifications = (): ReactElement => { { setPreferences({ ...preferences, @@ -200,7 +198,7 @@ export const SafeNotifications = (): ReactElement => { { registerNotifications( { @@ -235,7 +233,7 @@ export const SafeNotifications = (): ReactElement => { } - disabled={!isOwner || !isSafeRegistered} + disabled={!isOwner || !preferences} />
From 4e650356f3e6196e9a9bc1ffc89145bd8b6e418b Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 09:23:33 +0200 Subject: [PATCH 20/62] fix: update tests --- package.json | 1 + .../Notifications/GlobalNotifications.tsx | 5 +- .../useNotificationPreferences.test.ts | 292 +++++++++++ .../useNotificationRegistrations.test.ts | 405 +++++++++++++++ .../hooks/useNotificationPreferences.ts | 12 +- .../hooks/useNotificationRegistrations.ts | 7 +- .../settings/Notifications/logic.test.ts | 463 ++++-------------- .../settings/Notifications/logic.ts | 18 +- yarn.lock | 65 ++- 9 files changed, 877 insertions(+), 391 deletions(-) create mode 100644 src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts create mode 100644 src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts diff --git a/package.json b/package.json index e2d0bf6c81..09a6c3add4 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-unused-imports": "^2.0.0", + "fake-indexeddb": "^4.0.2", "jest": "^28.1.2", "jest-environment-jsdom": "^28.1.2", "pre-commit": "^1.2.2", diff --git a/src/components/settings/Notifications/GlobalNotifications.tsx b/src/components/settings/Notifications/GlobalNotifications.tsx index 307e9d347b..fb3ec372e1 100644 --- a/src/components/settings/Notifications/GlobalNotifications.tsx +++ b/src/components/settings/Notifications/GlobalNotifications.tsx @@ -19,17 +19,16 @@ import { sameAddress } from '@/utils/addresses' import useChains from '@/hooks/useChains' import { useAppSelector } from '@/store' import CheckWallet from '@/components/common/CheckWallet' -import { requestNotificationPermission } from './logic' import { useNotificationPreferences } from './hooks/useNotificationPreferences' import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' import { selectAllAddedSafes } from '@/store/addedSafesSlice' +import { requestNotificationPermission } from './logic' +import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { NotificationPreferences } from './hooks/notifications-idb' import css from './styles.module.css' -type NotifiableSafes = { [chainId: string]: Array } - // Convert data structure of added Safes export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafes => { const obj: NotifiableSafes = {} diff --git a/src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts new file mode 100644 index 0000000000..8eb16fc9cb --- /dev/null +++ b/src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -0,0 +1,292 @@ +import 'fake-indexeddb/auto' +import { set, setMany } from 'idb-keyval' +import { renderHook, waitFor } from '@/tests/test-utils' +import { hexZeroPad } from 'ethers/lib/utils' + +import { createUuidStore, createPreferencesStore } from '../notifications-idb' +import { + useNotificationPreferences, + _DEFAULT_NOTIFICATION_PREFERENCES, + _setPreferences, + _setUuid, +} from '../useNotificationPreferences' +import { WebhookType } from '@/services/firebase/webhooks' + +Object.defineProperty(globalThis, 'crypto', { + value: { + randomUUID: () => Math.random().toString(), + }, +}) + +describe('useNotificationPreferences', () => { + beforeEach(() => { + // Reset indexedDB + indexedDB = new IDBFactory() + }) + + describe('uuidStore', () => { + beforeEach(() => { + _setUuid(undefined) + }) + + it('should initialise uuid if it does not exist', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.uuid).toEqual(expect.any(String)) + }) + }) + + it('return uuid if it exists', async () => { + const uuid = 'test-uuid' + + await set('uuid', uuid, createUuidStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.uuid).toEqual(uuid) + }) + }) + }) + + describe('preferencesStore', () => { + beforeEach(() => { + _setPreferences(undefined) + }) + + it('should return all existing preferences', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + const preferences = { + [`${chainId}:${safeAddress}`]: { + chainId, + safeAddress, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await setMany(Object.entries(preferences), createPreferencesStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual(preferences) + }) + }) + + it('should return existing Safe preferences', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + const preferences = { + [`${chainId}:${safeAddress}`]: { + chainId, + safeAddress, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await setMany(Object.entries(preferences), createPreferencesStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + await waitFor(() => { + expect(result.current.getPreferences(chainId, safeAddress)).toEqual( + preferences[`${chainId}:${safeAddress}`].preferences, + ) + }) + }) + + it('should create preferences, then hydrate the preferences state', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) + + const chainId2 = '2' + + result.current._createPreferences({ + [chainId1]: [safeAddress1, safeAddress2], + [chainId2]: [safeAddress1], + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) + }) + }) + + it('should update preferences, then hydrate the preferences state', async () => { + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + const preferences = { + [`${chainId}:${safeAddress}`]: { + chainId: chainId, + safeAddress: safeAddress, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await setMany(Object.entries(preferences), createPreferencesStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current.updatePreferences(chainId, safeAddress, { + ..._DEFAULT_NOTIFICATION_PREFERENCES, + [WebhookType.NEW_CONFIRMATION]: false, + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId}:${safeAddress}`]: { + chainId: chainId, + safeAddress: safeAddress, + preferences: { + ..._DEFAULT_NOTIFICATION_PREFERENCES, + [WebhookType.NEW_CONFIRMATION]: false, + }, + }, + }) + }) + }) + + it('should delete preferences, then hydrate the preferences state', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) + + const chainId2 = '2' + + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await setMany(Object.entries(preferences), createPreferencesStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current._deletePreferences({ + [chainId1]: [safeAddress1, safeAddress2], + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({ + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + }) + }) + }) + + it('should clearPreferences preferences, then hydrate the preferences state', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) + + const chainId2 = '2' + + const preferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await setMany(Object.entries(preferences), createPreferencesStore()) + + const { result } = renderHook(() => useNotificationPreferences()) + + result.current._deletePreferences({ + [chainId1]: [safeAddress1, safeAddress2], + }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual(undefined) + }) + }) + + it('should hydrate accross instances', async () => { + const chainId1 = '1' + const safeAddress1 = hexZeroPad('0x1', 20) + const safeAddress2 = hexZeroPad('0x1', 20) + + const chainId2 = '2' + const { result: instance1 } = renderHook(() => useNotificationPreferences()) + const { result: instance2 } = renderHook(() => useNotificationPreferences()) + + instance1.current._createPreferences({ + [chainId1]: [safeAddress1, safeAddress2], + [chainId2]: [safeAddress1], + }) + + const expectedPreferences = { + [`${chainId1}:${safeAddress1}`]: { + chainId: chainId1, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId1}:${safeAddress2}`]: { + chainId: chainId1, + safeAddress: safeAddress2, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + [`${chainId2}:${safeAddress1}`]: { + chainId: chainId2, + safeAddress: safeAddress1, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + }, + } + + await waitFor(() => { + expect(instance1.current.getAllPreferences()).toEqual(expectedPreferences) + expect(instance2.current.getAllPreferences()).toEqual(expectedPreferences) + }) + }) + }) +}) diff --git a/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts new file mode 100644 index 0000000000..320fd99109 --- /dev/null +++ b/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -0,0 +1,405 @@ +import { hexZeroPad } from 'ethers/lib/utils' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' + +import { renderHook } from '@/tests/test-utils' +import { useNotificationRegistrations } from '../useNotificationRegistrations' +import * as logic from '../../logic' +import * as preferences from '../useNotificationPreferences' +import * as notificationsSlice from '@/store/notificationsSlice' +import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' + +jest.mock('@safe-global/safe-gateway-typescript-sdk') + +jest.mock('../useNotificationPreferences') + +Object.defineProperty(globalThis, 'crypto', { + value: { + randomUUID: () => Math.random().toString(), + }, +}) + +describe('useNotificationRegistrations', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('registerNotifications', () => { + const registerDeviceSpy = jest.spyOn(sdk, 'registerDevice') + + const examplePayload: logic.NotificationRegistration = { + uuid: self.crypto.randomUUID(), + cloudMessagingToken: 'token', + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: DeviceType.WEB, + version: '1.17.0', + timestamp: Math.floor(new Date().getTime() / 1000).toString(), + safeRegistrations: [ + { + chainId: '1', + safes: [hexZeroPad('0x1', 20)], + signatures: [], + }, + ], + } + + it('does not register if no uuid is present', async () => { + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: undefined, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications({}) + + expect(registerDeviceSpy).not.toHaveBeenCalled() + }) + + it('does not create preferences/notify if registration does not succeed', async () => { + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + + // @ts-expect-error + registerDeviceSpy.mockImplementation(() => Promise.resolve('Registration could not be completed.')) + + const createPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: self.crypto.randomUUID(), + _createPreferences: createPreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications({ + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + }) + + expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + + expect(createPreferencesMock).not.toHaveBeenCalled() + }) + + it('does not create preferences/notify if registration throws', async () => { + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + + // @ts-expect-error + registerDeviceSpy.mockImplementation(() => Promise.resolve('Registration could not be completed.')) + + const createPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: self.crypto.randomUUID(), + _createPreferences: createPreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications({ + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + }) + + expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + + expect(createPreferencesMock).not.toHaveBeenCalledWith() + }) + + it('creates preferences if registration succeeds without signature for a single Safe Account', async () => { + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + + registerDeviceSpy.mockImplementation(() => Promise.resolve()) + + const createPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: self.crypto.randomUUID(), + _createPreferences: createPreferencesMock, + } as unknown as ReturnType), + ) + + const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications({ + '1': [hexZeroPad('0x1', 20)], + }) + + expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + + expect(createPreferencesMock).toHaveBeenCalled() + + expect(showNotificationSpy).toHaveBeenCalledWith({ + message: 'You will now receive notifications for this Safe Account in your browser.', + variant: 'success', + groupKey: 'notifications', + }) + }) + + it('creates preferences if registration succeeds without signature for multiple Safe Accounts', async () => { + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + + const createPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: self.crypto.randomUUID(), + _createPreferences: createPreferencesMock, + } as unknown as ReturnType), + ) + + const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications({ + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + }) + + expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + + expect(createPreferencesMock).toHaveBeenCalled() + + expect(showNotificationSpy).toHaveBeenCalledWith({ + message: 'You will now receive notifications for these Safe Accounts in your browser.', + variant: 'success', + groupKey: 'notifications', + }) + }) + + it('creates preferences/does not notify if registration succeeded with signature', async () => { + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + + registerDeviceSpy.mockImplementation(() => Promise.resolve()) + + const createPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: self.crypto.randomUUID(), + _createPreferences: createPreferencesMock, + } as unknown as ReturnType), + ) + + const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.registerNotifications( + { + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + }, + true, + ) + + expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + + expect(createPreferencesMock).toHaveBeenCalled() + + expect(showNotificationSpy).not.toHaveBeenCalled() + }) + }) + + describe('unregisterSafeNotifications', () => { + const unregisterSafeSpy = jest.spyOn(sdk, 'unregisterSafe') + + it('does not unregister if no uuid is present', async () => { + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: undefined, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + await result.current.unregisterSafeNotifications('1', hexZeroPad('0x1', 20)) + + expect(unregisterSafeSpy).not.toHaveBeenCalled() + }) + + it('does not delete preferences if unregistration does not succeed', async () => { + // @ts-expect-error + unregisterSafeSpy.mockImplementation(() => Promise.resolve('Unregistration could not be completed.')) + + const uuid = self.crypto.randomUUID() + const deletePreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _deletePreferences: deletePreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + await result.current.unregisterSafeNotifications(chainId, safeAddress) + + expect(unregisterSafeSpy).toHaveBeenCalledWith(chainId, safeAddress, uuid) + + expect(deletePreferencesMock).not.toHaveBeenCalled() + }) + + it('does not delete preferences if unregistration throws', async () => { + unregisterSafeSpy.mockImplementation(() => Promise.reject()) + + const uuid = self.crypto.randomUUID() + const deletePreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _deletePreferences: deletePreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + await result.current.unregisterSafeNotifications(chainId, safeAddress) + + expect(unregisterSafeSpy).toHaveBeenCalledWith(chainId, safeAddress, uuid) + + expect(deletePreferencesMock).not.toHaveBeenCalled() + }) + + it('deletes preferences if unregistration succeeds', async () => { + unregisterSafeSpy.mockImplementation(() => Promise.resolve()) + + const uuid = self.crypto.randomUUID() + const deletePreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _deletePreferences: deletePreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + const chainId = '1' + const safeAddress = hexZeroPad('0x1', 20) + + await result.current.unregisterSafeNotifications(chainId, safeAddress) + + expect(unregisterSafeSpy).toHaveBeenCalledWith(chainId, safeAddress, uuid) + + expect(deletePreferencesMock).toHaveBeenCalledWith({ [chainId]: [safeAddress] }) + }) + }) + + describe('unregisterAllNotifications', () => { + const unregisterDeviceSpy = jest.spyOn(sdk, 'unregisterDevice') + + it('does not unregister device if no uuid is present', () => { + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid: undefined, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + result.current.unregisterAllNotifications() + + expect(unregisterDeviceSpy).not.toHaveBeenCalled() + }) + + it('does not clear preferences if unregistration does not succeed', () => { + // @ts-expect-error + unregisterDeviceSpy.mockImplementation(() => Promise.resolve('Unregistration could not be completed.')) + + const uuid = self.crypto.randomUUID() + const clearPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _clearPreferences: clearPreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + result.current.unregisterAllNotifications() + + expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) + + expect(clearPreferencesMock).not.toHaveBeenCalled() + }) + + it('does not clear preferences if unregistration throws', () => { + unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) + + const uuid = self.crypto.randomUUID() + const clearPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _clearPreferences: clearPreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + result.current.unregisterAllNotifications() + + expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) + + expect(clearPreferencesMock).not.toHaveBeenCalledWith() + }) + + it('clears preferences if unregistration succeeds', () => { + unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) + + const uuid = self.crypto.randomUUID() + const clearPreferencesMock = jest.fn() + + ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( + () => + ({ + uuid, + _clearPreferences: clearPreferencesMock, + } as unknown as ReturnType), + ) + + const { result } = renderHook(() => useNotificationRegistrations()) + + result.current.unregisterAllNotifications() + + expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) + + expect(clearPreferencesMock).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts index 1e3795211c..d49b225a7e 100644 --- a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/Notifications/hooks/useNotificationPreferences.ts @@ -6,7 +6,7 @@ import ExternalStore from '@/services/ExternalStore' import { createPreferencesStore, createUuidStore, getSafeNotificationKey } from './notifications-idb' import type { NotificationPreferences, SafeNotificationKey } from './notifications-idb' -const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationKey]['preferences'] = { +export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationKey]['preferences'] = { [WebhookType.NEW_CONFIRMATION]: true, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, @@ -19,10 +19,14 @@ const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotification [WebhookType.SAFE_CREATED]: false, // Cannot be registered to predicted address } -// ExternalStores are used to keep indexedDB state longer than the component lifecycle +// ExternalStores are used to keep indexedDB state synced across hook instances const { useStore: useUuid, setStore: setUuid } = new ExternalStore() const { useStore: usePreferences, setStore: setPreferences } = new ExternalStore() +// Used for testing +export const _setUuid = setUuid +export const _setPreferences = setPreferences + export const useNotificationPreferences = () => { // State const uuid = useUuid() @@ -112,7 +116,7 @@ export const useNotificationPreferences = () => { const defaultPreferences: NotificationPreferences[SafeNotificationKey] = { chainId, safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, } return [key, defaultPreferences] @@ -177,8 +181,8 @@ export const useNotificationPreferences = () => { uuid, getAllPreferences, getPreferences, - updatePreferences, _createPreferences: createPreferences, + updatePreferences, _deletePreferences: deletePreferences, _clearPreferences: clearPreferences, } diff --git a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts index 6c15ca57aa..dc09b7eed2 100644 --- a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts @@ -3,10 +3,9 @@ import { registerDevice, unregisterDevice, unregisterSafe } from '@safe-global/s import { useWeb3 } from '@/hooks/wallets/web3' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' -import { getRegisterDevicePayload } from '../logic' import { useNotificationPreferences } from './useNotificationPreferences' - -type Subscriptions = { [chainId: string]: Array } +import { getRegisterDevicePayload } from '../logic' +import type { NotifiableSafes } from '../logic' export const useNotificationRegistrations = () => { const dispatch = useAppDispatch() @@ -14,7 +13,7 @@ export const useNotificationRegistrations = () => { const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences() - const registerNotifications = async (safesToRegister: Subscriptions, withSignature = false) => { + const registerNotifications = async (safesToRegister: NotifiableSafes, withSignature = false) => { if (!uuid) { return } diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/Notifications/logic.test.ts index 471622a76c..c15176a453 100644 --- a/src/components/settings/Notifications/logic.test.ts +++ b/src/components/settings/Notifications/logic.test.ts @@ -1,11 +1,14 @@ +import * as firebase from 'firebase/messaging' +import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' +import { hexZeroPad } from 'ethers/lib/utils' import { Web3Provider } from '@ethersproject/providers' +import type { JsonRpcSigner } from '@ethersproject/providers' import * as logic from './logic' +import packageJson from '../../../../package.json' jest.mock('firebase/messaging') -jest.mock('@safe-global/safe-gateway-typescript-sdk') - Object.defineProperty(globalThis, 'crypto', { value: { randomUUID: () => Math.random().toString(), @@ -26,8 +29,6 @@ Object.defineProperty(globalThis, 'location', { }, }) -const mockProvider = new Web3Provider(jest.fn()) - describe('Notifications', () => { let alertMock = jest.fn() @@ -87,371 +88,91 @@ describe('Notifications', () => { }) }) - // TODO: - - // describe('createRegisterDevicePayload', () => { - // it('should return the current registration if it is the same', async () => { - // const token = crypto.randomUUID() - // const signature = hexZeroPad('0xDEAD', 65) - - // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - // jest.spyOn(mockProvider, 'getSigner').mockImplementation( - // () => - // ({ - // signMessage: jest.fn().mockResolvedValue(signature), - // } as unknown as JsonRpcSigner), - // ) - - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // const currentRegistration: logic.NotificationRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: token, - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: crypto.randomUUID(), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress], - // signatures: [hexZeroPad('0xDEAD', 65)], - // }, - // ], - // } - - // const payload = await logic.createRegisterDevicePayload( - // { [chainId]: [safeAddress] }, - // mockProvider, - // currentRegistration, - // ) - - // expect(payload).toStrictEqual({ ...currentRegistration, timestamp: expect.any(String) }) - // }) - - // describe('should return a registration payload if the chain registration(s) is not already registered', () => { - // it('if none on the same chain is registered', async () => { - // const token = crypto.randomUUID() - // const signature = hexZeroPad('0xDEAD', 65) - - // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - // jest.spyOn(mockProvider, 'getSigner').mockImplementation( - // () => - // ({ - // signMessage: jest.fn().mockResolvedValue(signature), - // } as unknown as JsonRpcSigner), - // ) - - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // const payload = await logic.createRegisterDevicePayload({ [chainId]: [safeAddress] }, mockProvider) - - // expect(payload).toStrictEqual({ - // uuid: expect.any(String), - // cloudMessagingToken: token, - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: expect.any(String), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress], - // signatures: [signature], - // }, - // ], - // }) - // }) - - // it('if others on the same chain exists', async () => { - // const token = crypto.randomUUID() - // const signature = hexZeroPad('0xDEAD', 65) - - // jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - // jest.spyOn(mockProvider, 'getSigner').mockImplementation( - // () => - // ({ - // signMessage: jest.fn().mockResolvedValue(signature), - // } as unknown as JsonRpcSigner), - // ) - - // const safeAddress = hexZeroPad('0x3', 20) - // const chainId = '1' - - // const currentRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: token, - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: expect.any(String), - // safeRegistrations: [ - // { - // chainId, - // safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], - // signatures: [hexZeroPad('0xBEEF', 65)], - // }, - // { - // chainId: '2', - // safes: [hexZeroPad('0x4', 20)], - // signatures: [signature], - // }, - // ], - // } - - // const payload = await logic.createRegisterDevicePayload( - // { [chainId]: [safeAddress] }, - // mockProvider, - // currentRegistration, - // ) - - // expect(payload.timestamp).not.toBe(currentRegistration.timestamp) - // expect(payload.safeRegistrations[0].signatures).not.toBe(currentRegistration.safeRegistrations[0].signatures) - - // expect(payload).toStrictEqual({ - // uuid: currentRegistration.uuid, // Same UUID - // cloudMessagingToken: expect.any(String), - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: expect.any(String), - // safeRegistrations: [ - // { - // chainId, - // safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20), safeAddress], - // signatures: [signature], - // }, - // { - // chainId: '2', - // safes: [hexZeroPad('0x4', 20)], - // signatures: [signature], - // }, - // ], - // }) - // }) - // }) - // }) - - // describe('registerNotifications', () => { - // const mockRegisterSafe = jest.spyOn(sdk, 'registerDevice') - - // it('should return undefined if no registration exists and the registration threw', async () => { - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - // jest - // .spyOn(logic, 'createRegisterDevicePayload') - // .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - // mockRegisterSafe.mockImplementation(() => { - // return Promise.reject() - // }) - - // const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) - - // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - // expect(alertMock).toHaveBeenCalledTimes(1) - // expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') - - // expect(registration).toBe(undefined) - // }) - - // it('should return the current registration if one exists and the registration threw', async () => { - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - // jest - // .spyOn(logic, 'createRegisterDevicePayload') - // .mockImplementation(() => Promise.resolve({} as logic.NotificationRegistration)) - - // mockRegisterSafe.mockImplementation(() => { - // return Promise.reject() - // }) - - // const currentRegistration: logic.NotificationRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: crypto.randomUUID(), - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: crypto.randomUUID(), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress], - // signatures: [hexZeroPad('0xDEAD', 65)], - // }, - // ], - // } - - // const registration = await logic.registerNotifications( - // { [chainId]: [safeAddress] }, - // mockProvider, - // currentRegistration, - // ) - - // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - // expect(alertMock).toHaveBeenCalledTimes(1) - // expect(alertMock).toHaveBeenCalledWith('Unable to register Safe(s)') - - // expect(registration).toBe(currentRegistration) - // }) - - // it('should return the registration payload if the registration succeeded', async () => { - // jest.spyOn(logic, 'requestNotificationPermission').mockImplementation(() => Promise.resolve(true)) - - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // const registrationPayload: logic.NotificationRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: crypto.randomUUID(), - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: crypto.randomUUID(), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress], - // signatures: [hexZeroPad('0xDEAD', 65)], - // }, - // ], - // } - - // jest.spyOn(logic, 'createRegisterDevicePayload').mockImplementation(() => Promise.resolve(registrationPayload)) - - // mockRegisterSafe.mockImplementation(() => { - // return Promise.resolve() - // }) - - // const registration = await logic.registerNotifications({ [chainId]: [safeAddress] }, mockProvider) - - // expect(mockRegisterSafe).toHaveBeenCalledTimes(1) - - // expect(alertMock).not.toHaveBeenCalled() - - // expect(registration).not.toBe(registrationPayload) - // }) - // }) - - // describe('unregisterSafe', () => { - // const mockUnregisterSafe = jest.spyOn(sdk, 'unregisterSafe') - - // it('should return the current registration if the unregistration threw', async () => { - // mockUnregisterSafe.mockImplementation(() => { - // return Promise.reject() - // }) - - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // const currentRegistration: logic.NotificationRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: crypto.randomUUID(), - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: crypto.randomUUID(), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress], - // signatures: [hexZeroPad('0xDEAD', 65)], - // }, - // ], - // } - - // const updatedRegistration = await logic.unregisterSafe( - // { chainId, address: { value: safeAddress } } as SafeInfo, - // currentRegistration, - // ) - - // expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) - - // expect(alertMock).toHaveBeenCalledTimes(1) - // expect(alertMock).toHaveBeenCalledWith('Unable to unregister Safe') - - // expect(updatedRegistration).toEqual(currentRegistration) - // }) - - // it('should return the updated registration if the registration succeeded', async () => { - // mockUnregisterSafe.mockImplementation(() => { - // return Promise.resolve() - // }) - - // const safeAddress = hexZeroPad('0x1', 20) - // const chainId = '1' - - // const currentRegistration: logic.NotificationRegistration = { - // uuid: crypto.randomUUID(), - // cloudMessagingToken: crypto.randomUUID(), - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: crypto.randomUUID(), - // safeRegistrations: [ - // { - // chainId, - // safes: [safeAddress, hexZeroPad('0x2', 20)], - // signatures: [hexZeroPad('0xDEAD', 65)], - // }, - // { - // chainId: '5', - // safes: [safeAddress], // Same address Safe on a different chain - // signatures: [hexZeroPad('0xBEEF', 65)], - // }, - // ], - // } - - // const updatedRegistration = await logic.unregisterSafe( - // { chainId, address: { value: safeAddress } } as SafeInfo, - // currentRegistration, - // ) - - // expect(mockUnregisterSafe).toHaveBeenCalledTimes(1) - - // expect(alertMock).not.toHaveBeenCalled() - - // expect(updatedRegistration.timestamp).not.toBe(currentRegistration.timestamp) - - // expect(updatedRegistration).toEqual({ - // uuid: currentRegistration.uuid, // Same UUID - // cloudMessagingToken: currentRegistration.cloudMessagingToken, // Same token - // buildNumber: '0', - // bundle: 'https://app.safe.global', - // deviceType: DeviceType.WEB, - // version: packageJson.version, - // timestamp: expect.any(String), - // safeRegistrations: [ - // { - // chainId, - // safes: [hexZeroPad('0x2', 20)], - // signatures: [], - // }, - // { - // chainId: '5', - // safes: [safeAddress], // Same address Safe on a different chain - // signatures: [hexZeroPad('0xBEEF', 65)], - // }, - // ], - // }) - // }) - // }) + describe('getRegisterDevicePayload', () => { + it('should return the payload without signature', async () => { + const token = crypto.randomUUID() + + jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + + const uuid = crypto.randomUUID() + + const payload = await logic.getRegisterDevicePayload({ + safesToRegister: { + ['1']: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + ['2']: [hexZeroPad('0x1', 20)], + }, + uuid, + }) + + expect(payload).toStrictEqual({ + uuid, + cloudMessagingToken: token, + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId: '1', + safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + signatures: [], + }, + { + chainId: '2', + safes: [hexZeroPad('0x1', 20)], + signatures: [], + }, + ], + }) + }) + + it('should return the payload with signature', async () => { + const token = crypto.randomUUID() + jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) + + const mockProvider = new Web3Provider(jest.fn()) + const signature = hexZeroPad('0x69420', 65) + + jest.spyOn(mockProvider, 'getSigner').mockImplementation( + () => + ({ + signMessage: jest.fn().mockResolvedValueOnce(signature), + } as unknown as JsonRpcSigner), + ) + + const uuid = crypto.randomUUID() + + const payload = await logic.getRegisterDevicePayload({ + safesToRegister: { + ['1']: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + ['2']: [hexZeroPad('0x1', 20)], + }, + uuid, + web3: mockProvider, + }) + + expect(payload).toStrictEqual({ + uuid, + cloudMessagingToken: token, + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: DeviceType.WEB, + version: packageJson.version, + timestamp: expect.any(String), + safeRegistrations: [ + { + chainId: '1', + safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], + signatures: [signature], + }, + { + chainId: '2', + safes: [hexZeroPad('0x1', 20)], + signatures: [signature], + }, + ], + }) + }) + }) }) diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/Notifications/logic.ts index 7bfb24cd46..28ce0664ed 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/Notifications/logic.ts @@ -35,32 +35,34 @@ export const requestNotificationPermission = async (): Promise => { } const getSafeRegistrationSignature = ({ - safes, + safeAddresses, web3, timestamp, - deviceUuid, + uuid, token, }: { - safes: Array + safeAddresses: Array web3: Web3Provider timestamp: string - deviceUuid: string + uuid: string token: string }) => { const MESSAGE_PREFIX = 'gnosis-safe' - const message = MESSAGE_PREFIX + timestamp + deviceUuid + token + safes.join('') + const message = MESSAGE_PREFIX + timestamp + uuid + token + safeAddresses.join('') const hashedMessage = keccak256(toUtf8Bytes(message)) return web3.getSigner().signMessage(hashedMessage) } +export type NotifiableSafes = { [chainId: string]: Array } + export const getRegisterDevicePayload = async ({ safesToRegister, uuid, web3, }: { - safesToRegister: { [chainId: string]: Array } + safesToRegister: NotifiableSafes uuid: string web3?: Web3Provider }): Promise => { @@ -88,7 +90,7 @@ export const getRegisterDevicePayload = async ({ Object.entries(safesToRegister).map(async ([chainId, safeAddresses]) => { // Signature is only required for CONFIRMATION_REQUESTS const signature = web3 - ? await getSafeRegistrationSignature({ safes: safeAddresses, web3, deviceUuid: uuid, timestamp, token }) + ? await getSafeRegistrationSignature({ safeAddresses, web3, uuid, timestamp, token }) : undefined return { @@ -100,7 +102,7 @@ export const getRegisterDevicePayload = async ({ ) return { - uuid: uuid, + uuid, cloudMessagingToken: token, buildNumber: '0', // Required value, but does not exist on web bundle: location.origin, diff --git a/yarn.lock b/yarn.lock index 8d3d411f96..716fb60b54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6907,6 +6907,11 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base64-arraybuffer-es6@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86" + integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw== + base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -8346,6 +8351,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -9581,6 +9593,13 @@ eyes@^0.1.8: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== +fake-indexeddb@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.2.tgz#e7a884158fa576e00f03e973b9874619947013e4" + integrity sha512-SdTwEhnakbgazc7W3WUXOJfGmhH0YfG4d+dRPOFoYDRTL6U5t8tvrmkf2W/C3W1jk2ylV7Wrnj44RASqpX/lEw== + dependencies: + realistic-structured-clone "^3.0.0" + fake-merkle-patricia-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" @@ -11857,7 +11876,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -13490,6 +13509,15 @@ real-require@^0.1.0: resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381" integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg== +realistic-structured-clone@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-3.0.0.tgz#7b518049ce2dad41ac32b421cd297075b00e3e35" + integrity sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q== + dependencies: + domexception "^1.0.1" + typeson "^6.1.0" + typeson-registry "^1.0.0-alpha.20" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -14875,6 +14903,13 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + tr46@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" @@ -15125,6 +15160,20 @@ typescript@4.9.4, typescript@^4.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typeson-registry@^1.0.0-alpha.20: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211" + integrity sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw== + dependencies: + base64-arraybuffer-es6 "^0.7.0" + typeson "^6.0.0" + whatwg-url "^8.4.0" + +typeson@^6.0.0, typeson@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" + integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== + typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" @@ -15681,6 +15730,11 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -15793,6 +15847,15 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +whatwg-url@^8.4.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" From d7eb862315242dd2cc484550f06591d782273811 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 13:06:52 +0200 Subject: [PATCH 21/62] fix: preferences, env vars, not in UI + tweaks --- .env.example | 28 ++-- .github/workflows/build/action.yml | 27 ++-- README.md | 18 +++ public/firebase-messaging-sw.ts | 76 +++++---- .../NotificationBanner/styles.module.css | 12 +- .../useNotificationRegistrations.test.ts | 146 +++++++++++------- .../hooks/useNotificationRegistrations.ts | 24 ++- .../settings/Notifications/index.tsx | 8 +- src/config/constants.ts | 43 ++++-- src/hooks/useFirebaseNotifications.ts | 73 ++------- src/services/firebase/index.ts | 14 +- 11 files changed, 265 insertions(+), 204 deletions(-) diff --git a/.env.example b/.env.example index 400fd2a56e..f388430182 100644 --- a/.env.example +++ b/.env.example @@ -33,15 +33,25 @@ NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION= NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING= # Firebase Cloud Messaging -NEXT_PUBLIC_FIREBASE_API_KEY= -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= -NEXT_PUBLIC_FIREBASE_DATABASE_URL= -NEXT_PUBLIC_FIREBASE_PROJECT_ID= -NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= -NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= -NEXT_PUBLIC_FIREBASE_APP_ID= -NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= -NEXT_PUBLIC_FIREBASE_VAPID_KEY= +NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION= +NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION= +NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION= +NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION= +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION= +NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION= + +NEXT_PUBLIC_FIREBASE_API_KEY_STAGING= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING= +NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING= +NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING= +NEXT_PUBLIC_FIREBASE_APP_ID_STAGING= +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING= +NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= # Redefine NEXT_PUBLIC_REDEFINE_API= \ No newline at end of file diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 19c1b372ac..42031d612e 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,12 +43,21 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} - NEXT_PUBLIC_FIREBASE_API_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} - NEXT_PUBLIC_FIREBASE_DATABASE_URL: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} - NEXT_PUBLIC_FIREBASE_APP_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID }} - NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} - NEXT_PUBLIC_FIREBASE_VAPID_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY }} + NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_API_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY_STAGING }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING }} + NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING }} + NEXT_PUBLIC_FIREBASE_APP_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID_STAGING }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING }} + NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING }} diff --git a/README.md b/README.md index 89d7f458bc..f92d72e272 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,24 @@ Here's the list of all the required and optional variables: | `NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING` | optional | Relay URL on staging | `NEXT_PUBLIC_IS_OFFICIAL_HOST` | optional | Whether it's the official distribution of the app, or a fork; has legal implications. Set to true only if you also update the legal pages like Imprint and Terms of use | `NEXT_PUBLIC_REDEFINE_API` | optional | Redefine API base URL +| `NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION` | optional | Firebase Cloud Messaging (FCM) API key on production +| `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION` | optional | FCM auth domain on production +| `NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION` | optional | FCM database URL on production +| `NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION` | optional | FCM project ID on production +| `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION` | optional | FCM storage bucket on production +| `NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION` | optional | FCM sender ID on production +| `NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION` | optional | FCM app ID on production +| `NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION` | optional | FCM measurement ID on production +| `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | optional | FCM vapid key on production +| `NEXT_PUBLIC_FIREBASE_API_KEY_STAGING` | optional | FCM API key on staging +| `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING` | optional | FCM auth domain on staging +| `NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING` | optional | FCM database URL on staging +| `NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING` | optional | FCM project ID on staging +| `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING` | optional | FCM storage bucket on staging +| `NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING` | optional | FCM sender ID on staging +| `NEXT_PUBLIC_FIREBASE_APP_ID_STAGING` | optional | FCM app ID on staging +| `NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING` | optional | FCM measurement ID on staging +| `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | optional | FCM vapid key on staging If you don't provide some of the optional vars, the corresponding features will be disabled in the UI. diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 2a437bfd2a..e024a7f794 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -5,6 +5,7 @@ import { onBackgroundMessage } from 'firebase/messaging/sw' import { getMessaging } from 'firebase/messaging/sw' import { parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' +import { FIREBASE_OPTIONS } from '@/config/constants' // Default type of `self` is `WorkerGlobalScope & typeof globalThis` // https://github.com/microsoft/TypeScript/issues/14877 @@ -13,52 +14,47 @@ declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown } // Satisfy Workbox self.__WB_MANIFEST -// Must be called before `onBackgroundMessage` as Firebase embeds a `notificationclick` listener -self.addEventListener( - 'notificationclick', - (event) => { - event.notification.close() +const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) - const link = event.notification.tag +if (hasFirebaseOptions) { + // Must be called before `onBackgroundMessage` as Firebase embeds a `notificationclick` listener + self.addEventListener( + 'notificationclick', + (event) => { + event.notification.close() - if (link) { - self.clients.openWindow(link) - } - }, - false, -) + const link = event.notification.tag + + if (link) { + self.clients.openWindow(link) + } + }, + false, + ) -const app = initializeApp({ - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, -}) + const app = initializeApp(FIREBASE_OPTIONS) -const messaging = getMessaging(app) + const messaging = getMessaging(app) -onBackgroundMessage(messaging, async (payload) => { - const ICON_PATH = '/images/safe-logo-green.png' - const DEFAULT_LINK = 'https://app.safe.global' + onBackgroundMessage(messaging, async (payload) => { + const ICON_PATH = '/images/safe-logo-green.png' + const DEFAULT_LINK = 'https://app.safe.global' - const shouldShow = await shouldShowNotification(payload) + const shouldShow = await shouldShowNotification(payload) - if (!shouldShow) { - return - } + if (!shouldShow) { + return + } - const notification = await parseFirebaseNotification(payload) + const notification = await parseFirebaseNotification(payload) - if (notification) { - self.registration.showNotification(notification.title, { - icon: ICON_PATH, - body: notification.body, - image: notification.image, - tag: notification.link ?? DEFAULT_LINK, - }) - } -}) + if (notification) { + self.registration.showNotification(notification.title, { + icon: ICON_PATH, + body: notification.body, + image: notification.image, + tag: notification.link ?? DEFAULT_LINK, + }) + } + }) +} diff --git a/src/components/settings/Notifications/NotificationBanner/styles.module.css b/src/components/settings/Notifications/NotificationBanner/styles.module.css index 575c1463d3..a218c3ec50 100644 --- a/src/components/settings/Notifications/NotificationBanner/styles.module.css +++ b/src/components/settings/Notifications/NotificationBanner/styles.module.css @@ -1,13 +1,18 @@ .banner :global .MuiTooltip-tooltip { min-width: 384px !important; padding: var(--space-3); - border-color: var(--color-secondary-main); } +.banner :global .MuiTooltip-tooltip, .banner :global .MuiTooltip-arrow::before { border-color: var(--color-secondary-main); } +[data-theme='dark'] .banner :global .MuiTooltip-tooltip, +[data-theme='dark'] .banner :global .MuiTooltip-arrow::before { + border-color: var(--color-primary-main); +} + .container { min-width: 100%; } @@ -37,6 +42,11 @@ height: 24px; } +[data-theme='dark'] .chip { + color: var(--color-static-main); + background-color: var(--color-primary-main); +} + .chip :global .MuiChip-label { padding: 0; text-overflow: unset; diff --git a/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts index 320fd99109..2e64e4fe44 100644 --- a/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -1,4 +1,5 @@ import { hexZeroPad } from 'ethers/lib/utils' +import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import * as sdk from '@safe-global/safe-gateway-typescript-sdk' import { renderHook } from '@/tests/test-utils' @@ -6,7 +7,6 @@ import { useNotificationRegistrations } from '../useNotificationRegistrations' import * as logic from '../../logic' import * as preferences from '../useNotificationPreferences' import * as notificationsSlice from '@/store/notificationsSlice' -import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' jest.mock('@safe-global/safe-gateway-typescript-sdk') @@ -26,21 +26,34 @@ describe('useNotificationRegistrations', () => { describe('registerNotifications', () => { const registerDeviceSpy = jest.spyOn(sdk, 'registerDevice') - const examplePayload: logic.NotificationRegistration = { - uuid: self.crypto.randomUUID(), - cloudMessagingToken: 'token', - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: '1.17.0', - timestamp: Math.floor(new Date().getTime() / 1000).toString(), - safeRegistrations: [ - { - chainId: '1', - safes: [hexZeroPad('0x1', 20)], - signatures: [], - }, - ], + const getExampleRegisterDevicePayload = ( + safesToRegister: logic.NotifiableSafes, + withSignatures = false, + ): logic.NotificationRegistration => { + const safeRegistrations = Object.entries(safesToRegister).reduce< + logic.NotificationRegistration['safeRegistrations'] + >((acc, [chainId, safeAddresses]) => { + const safeRegistration: logic.NotificationRegistration['safeRegistrations'][number] = { + chainId, + safes: safeAddresses, + signatures: withSignatures ? [hexZeroPad('0x69420', 65)] : [], + } + + acc.push(safeRegistration) + + return acc + }, []) + + return { + uuid: self.crypto.randomUUID(), + cloudMessagingToken: 'token', + buildNumber: '0', + bundle: 'https://app.safe.global', + deviceType: DeviceType.WEB, + version: '1.17.0', + timestamp: Math.floor(new Date().getTime() / 1000).toString(), + safeRegistrations, + } } it('does not register if no uuid is present', async () => { @@ -59,7 +72,14 @@ describe('useNotificationRegistrations', () => { }) it('does not create preferences/notify if registration does not succeed', async () => { - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + const safesToRegister: logic.NotifiableSafes = { + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + } + + const payload = getExampleRegisterDevicePayload(safesToRegister) + + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) // @ts-expect-error registerDeviceSpy.mockImplementation(() => Promise.resolve('Registration could not be completed.')) @@ -76,18 +96,22 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications({ - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - }) + await result.current.registerNotifications(safesToRegister) - expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).not.toHaveBeenCalled() }) it('does not create preferences/notify if registration throws', async () => { - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + const safesToRegister: logic.NotifiableSafes = { + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + } + + const payload = getExampleRegisterDevicePayload(safesToRegister) + + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) // @ts-expect-error registerDeviceSpy.mockImplementation(() => Promise.resolve('Registration could not be completed.')) @@ -104,18 +128,21 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications({ - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - }) + await result.current.registerNotifications(safesToRegister) - expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).not.toHaveBeenCalledWith() }) it('creates preferences if registration succeeds without signature for a single Safe Account', async () => { - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + const safesToRegister: logic.NotifiableSafes = { + '1': [hexZeroPad('0x1', 20)], + } + + const payload = getExampleRegisterDevicePayload(safesToRegister) + + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) registerDeviceSpy.mockImplementation(() => Promise.resolve()) @@ -133,11 +160,9 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications({ - '1': [hexZeroPad('0x1', 20)], - }) + await result.current.registerNotifications(safesToRegister) - expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).toHaveBeenCalled() @@ -149,7 +174,14 @@ describe('useNotificationRegistrations', () => { }) it('creates preferences if registration succeeds without signature for multiple Safe Accounts', async () => { - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + const safesToRegister: logic.NotifiableSafes = { + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + } + + const payload = getExampleRegisterDevicePayload(safesToRegister) + + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) const createPreferencesMock = jest.fn() @@ -165,12 +197,9 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications({ - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - }) + await result.current.registerNotifications(safesToRegister) - expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).toHaveBeenCalled() @@ -182,7 +211,14 @@ describe('useNotificationRegistrations', () => { }) it('creates preferences/does not notify if registration succeeded with signature', async () => { - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(examplePayload)) + const safesToRegister: logic.NotifiableSafes = { + '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], + } + + const payload = getExampleRegisterDevicePayload(safesToRegister) + + jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) registerDeviceSpy.mockImplementation(() => Promise.resolve()) @@ -200,15 +236,9 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications( - { - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - }, - true, - ) + await result.current.registerNotifications(safesToRegister, true) - expect(registerDeviceSpy).toHaveBeenCalledWith(examplePayload) + expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).toHaveBeenCalled() @@ -317,7 +347,7 @@ describe('useNotificationRegistrations', () => { describe('unregisterAllNotifications', () => { const unregisterDeviceSpy = jest.spyOn(sdk, 'unregisterDevice') - it('does not unregister device if no uuid is present', () => { + it('does not unregister device if no uuid is present', async () => { ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ @@ -327,12 +357,12 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - result.current.unregisterAllNotifications() + await result.current.unregisterAllNotifications() expect(unregisterDeviceSpy).not.toHaveBeenCalled() }) - it('does not clear preferences if unregistration does not succeed', () => { + it('does not clear preferences if unregistration does not succeed', async () => { // @ts-expect-error unregisterDeviceSpy.mockImplementation(() => Promise.resolve('Unregistration could not be completed.')) @@ -349,15 +379,15 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - result.current.unregisterAllNotifications() + await result.current.unregisterAllNotifications() expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) expect(clearPreferencesMock).not.toHaveBeenCalled() }) - it('does not clear preferences if unregistration throws', () => { - unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) + it('does not clear preferences if unregistration throws', async () => { + unregisterDeviceSpy.mockImplementation(() => Promise.reject()) const uuid = self.crypto.randomUUID() const clearPreferencesMock = jest.fn() @@ -372,14 +402,14 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - result.current.unregisterAllNotifications() + await result.current.unregisterAllNotifications() expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) expect(clearPreferencesMock).not.toHaveBeenCalledWith() }) - it('clears preferences if unregistration succeeds', () => { + it('clears preferences if unregistration succeeds', async () => { unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) const uuid = self.crypto.randomUUID() @@ -395,11 +425,11 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - result.current.unregisterAllNotifications() + await result.current.unregisterAllNotifications() expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(clearPreferencesMock).not.toHaveBeenCalled() + expect(clearPreferencesMock).toHaveBeenCalled() }) }) }) diff --git a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts index dc09b7eed2..8a0d508c8c 100644 --- a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts @@ -78,17 +78,27 @@ export const useNotificationRegistrations = () => { } } - const unregisterAllNotifications = () => { + const unregisterAllNotifications = async () => { if (!uuid) { return } - // Device unregistration is chain agnostic - unregisterDevice('1', uuid) - .then(() => { - _clearPreferences() - }) - .catch(() => null) + // Device unregistration is chain agnostic but is required for the route + const CHAIN_ID = '1' + + let didUnregister = false + + try { + const response = await unregisterDevice(CHAIN_ID, uuid) + + didUnregister = response == null + } catch (e) { + console.error(`Error unregistering device on chain ${CHAIN_ID}`, e) + } + + if (didUnregister) { + _clearPreferences() + } } return { diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/Notifications/index.tsx index 1e070f90ef..dc5e6dbdf2 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/Notifications/index.tsx @@ -228,9 +228,11 @@ export const SafeNotifications = (): ReactElement => { label={ <> Confirmation requests - - {isOwner ? 'Requires your signature' : 'Only owners'} - + {!preferences[WebhookType.CONFIRMATION_REQUEST] && ( + + {isOwner ? 'Requires your signature' : 'Only owners'} + + )} } disabled={!isOwner || !preferences} diff --git a/src/config/constants.ts b/src/config/constants.ts index 1b5fdde94f..afbe284f5c 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,4 +1,5 @@ import chains from './chains' +import type { FirebaseOptions } from 'firebase/app' export const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION export const IS_DEV = process.env.NODE_ENV === 'development' @@ -47,15 +48,39 @@ export const GOOGLE_TAG_MANAGER_AUTH_LATEST = process.env.NEXT_PUBLIC_GOOGLE_TAG export const GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH || '' // Firebase Cloud Messaging -export const FIREBASE_API_KEY = process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '' -export const FIREBASE_AUTH_DOMAIN = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || '' -export const FIREBASE_DATABASE_URL = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL || '' -export const FIREBASE_PROJECT_ID = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || '' -export const FIREBASE_STORAGE_BUCKET = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || '' -export const FIREBASE_MESSAGING_SENDER_ID = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || '' -export const FIREBASE_APP_ID = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || '' -export const FIREBASE_MEASUREMENT_ID = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID || '' -export const FIREBASE_VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY || '' +const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' +const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' +const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' +const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' +const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' +const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' +const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' +const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' +const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' + +const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' +const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' +const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' +const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' +const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' +const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' +const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' +const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' +const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' + +export const FIREBASE_OPTIONS: FirebaseOptions = { + apiKey: IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, + authDomain: IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, + databaseURL: IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, + projectId: IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, + storageBucket: IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, + messagingSenderId: IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, + appId: IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, + measurementId: IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, +} as const + +export const FIREBASE_VAPID_KEY = IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING + export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' // Tenderly - API docs: https://www.notion.so/Simulate-API-Documentation-6f7009fe6d1a48c999ffeb7941efc104 diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index abc6e5f039..ad629367ed 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -1,31 +1,23 @@ import { useEffect } from 'react' -import { initializeApp } from 'firebase/app' -import { getMessaging, onMessage } from 'firebase/messaging' - -import { useAppDispatch } from '@/store' -import { showNotification } from '@/store/notificationsSlice' -import { - FIREBASE_API_KEY, - FIREBASE_APP_ID, - FIREBASE_AUTH_DOMAIN, - FIREBASE_DATABASE_URL, - FIREBASE_MEASUREMENT_ID, - FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_MESSAGING_SW_PATH, - FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET, -} from '@/config/constants' -import { parseFirebaseNotification } from '@/services/firebase' -export const useFirebaseNotifications = (): null => { - const dispatch = useAppDispatch() +import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_OPTIONS } from '@/config/constants' +import { initializeApp } from 'firebase/app' +export const useFirebaseNotifications = (): void => { // Register servicer worker useEffect(() => { if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { return } + const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) + + if (!hasFirebaseOptions) { + return + } + + initializeApp(FIREBASE_OPTIONS) + const registerFirebaseSw = () => { navigator.serviceWorker.register(FIREBASE_MESSAGING_SW_PATH).catch(() => null) } @@ -36,47 +28,4 @@ export const useFirebaseNotifications = (): null => { window.removeEventListener('load', registerFirebaseSw) } }, []) - - // Listen for messages - useEffect(() => { - if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { - return - } - - const _app = initializeApp({ - apiKey: FIREBASE_API_KEY, - authDomain: FIREBASE_AUTH_DOMAIN, - databaseURL: FIREBASE_DATABASE_URL, - projectId: FIREBASE_PROJECT_ID, - storageBucket: FIREBASE_STORAGE_BUCKET, - messagingSenderId: FIREBASE_MESSAGING_SENDER_ID, - appId: FIREBASE_APP_ID, - measurementId: FIREBASE_MEASUREMENT_ID, - }) - - const messaging = getMessaging(_app) - - const unsubscribe = onMessage(messaging, async (payload) => { - const notification = await parseFirebaseNotification(payload) - - if (!notification) { - return - } - - dispatch( - showNotification({ - message: notification.title, - detailedMessage: notification.body, - groupKey: payload.messageId, - variant: 'info', - }), - ) - }) - - return () => { - unsubscribe() - } - }, [dispatch]) - - return null } diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 927e28dcc5..365697d46b 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -12,7 +12,10 @@ import { createPreferencesStore, getSafeNotificationKey, } from '@/components/settings/Notifications/hooks/notifications-idb' -import type { NotificationPreferences } from '@/components/settings/Notifications/hooks/notifications-idb' +import type { + NotificationPreferences, + SafeNotificationKey, +} from '@/components/settings/Notifications/hooks/notifications-idb' export const shouldShowNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { @@ -21,17 +24,16 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise(chainId, store).catch(() => null) + + const preferencesStore = await get(key, store).catch(() => null) if (!preferencesStore) { return false } - const key = getSafeNotificationKey(chainId, address) - const notificationPreferences = preferencesStore[key]?.preferences - - return !!notificationPreferences[type] + return preferencesStore.preferences[type] } // localStorage cannot be accessed in service workers so we reference the flag From 0e8facd0e6f910fdc41e2c3fbe29ac383535baeb Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 13:52:02 +0200 Subject: [PATCH 22/62] feat: add deep link to notification --- src/services/firebase/index.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 365697d46b..7b6240741e 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -89,7 +89,11 @@ export const _parseWebhookNotification = async ( const shortSafeAddress = shortenAddress(address) const historyLink = getLink(AppRoutes.transactions.history, address, chain) - const queueLink = getLink(AppRoutes.transactions.queue, address, chain) + + const getSafeTxHashLink = (safeTxHash: string): string => { + const txLink = getLink(AppRoutes.transactions.tx, address, chain) + return `${txLink}&id=${safeTxHash}` + } if (type === WebhookType.NEW_CONFIRMATION) { const { owner, safeTxHash } = data @@ -99,12 +103,12 @@ export const _parseWebhookNotification = async ( body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation from ${shortenAddress( owner, )} on transaction ${shortenAddress(safeTxHash)}.`, - link: queueLink, + link: getSafeTxHashLink(safeTxHash), } } if (type === WebhookType.EXECUTED_MULTISIG_TRANSACTION) { - const { failed, txHash } = data + const { failed, txHash, safeTxHash } = data const shortTxHash = shortenAddress(txHash) @@ -112,13 +116,13 @@ export const _parseWebhookNotification = async ( return { title: `Transaction failed`, body: `Safe ${shortSafeAddress} on ${chainName} failed to execute transaction ${shortTxHash}.`, - link: queueLink, + link: getSafeTxHashLink(safeTxHash), } } else { return { title: `Transaction executed`, body: `Safe ${shortSafeAddress} on ${chainName} executed transaction ${shortTxHash}.`, - link: historyLink, + link: getSafeTxHashLink(safeTxHash), } } } @@ -129,7 +133,7 @@ export const _parseWebhookNotification = async ( return { title: `New pending transaction`, body: `Safe ${shortSafeAddress} on ${chainName} has a new pending transaction ${shortenAddress(safeTxHash)}.`, - link: queueLink, + link: getSafeTxHashLink(safeTxHash), } } @@ -213,7 +217,7 @@ export const _parseWebhookNotification = async ( body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation request for transaction ${shortenAddress( safeTxHash, )}.`, - link: queueLink, + link: getSafeTxHashLink(safeTxHash), } } From 72a6353480020e8871265f1df7d4748881dac54b Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 14:32:39 +0200 Subject: [PATCH 23/62] fix: add tracking + rename to push notifications --- public/firebase-messaging-sw.ts | 32 +++-- src/components/common/Header/index.tsx | 6 +- .../GlobalPushNotifications.tsx} | 6 +- .../PushNotificationsBanner}/index.tsx | 22 +++- .../styles.module.css | 0 .../useNotificationPreferences.test.ts | 0 .../useNotificationRegistrations.test.ts | 0 .../hooks/notifications-idb.ts | 0 .../hooks/useNotificationPreferences.ts | 0 .../hooks/useNotificationRegistrations.ts | 30 +++-- .../index.tsx | 24 +++- .../logic.test.ts | 0 .../logic.ts | 4 + .../styles.module.css | 0 src/pages/settings/notifications.tsx | 4 +- .../analytics/events/push-notifications.ts | 109 ++++++++++++++++++ src/services/firebase/index.ts | 4 +- 17 files changed, 209 insertions(+), 32 deletions(-) rename src/components/settings/{Notifications/GlobalNotifications.tsx => PushNotifications/GlobalPushNotifications.tsx} (97%) rename src/components/settings/{Notifications/NotificationBanner => PushNotifications/PushNotificationsBanner}/index.tsx (82%) rename src/components/settings/{Notifications/NotificationBanner => PushNotifications/PushNotificationsBanner}/styles.module.css (100%) rename src/components/settings/{Notifications => PushNotifications}/hooks/__tests__/useNotificationPreferences.test.ts (100%) rename src/components/settings/{Notifications => PushNotifications}/hooks/__tests__/useNotificationRegistrations.test.ts (100%) rename src/components/settings/{Notifications => PushNotifications}/hooks/notifications-idb.ts (100%) rename src/components/settings/{Notifications => PushNotifications}/hooks/useNotificationPreferences.ts (100%) rename src/components/settings/{Notifications => PushNotifications}/hooks/useNotificationRegistrations.ts (79%) rename src/components/settings/{Notifications => PushNotifications}/index.tsx (88%) rename src/components/settings/{Notifications => PushNotifications}/logic.test.ts (100%) rename src/components/settings/{Notifications => PushNotifications}/logic.ts (93%) rename src/components/settings/{Notifications => PushNotifications}/styles.module.css (100%) create mode 100644 src/services/analytics/events/push-notifications.ts diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index e024a7f794..903dfb9096 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -6,6 +6,9 @@ import { getMessaging } from 'firebase/messaging/sw' import { parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' import { FIREBASE_OPTIONS } from '@/config/constants' +import { trackEvent } from '@/services/analytics' +import { isWebhookEvent } from '@/services/firebase/webhooks' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' // Default type of `self` is `WorkerGlobalScope & typeof globalThis` // https://github.com/microsoft/TypeScript/issues/14877 @@ -25,9 +28,13 @@ if (hasFirebaseOptions) { const link = event.notification.tag - if (link) { - self.clients.openWindow(link) + if (!link) { + return } + + self.clients.openWindow(link) + + trackEvent(PUSH_NOTIFICATION_EVENTS.CLICK_NOTIFICATION) }, false, ) @@ -48,13 +55,20 @@ if (hasFirebaseOptions) { const notification = await parseFirebaseNotification(payload) - if (notification) { - self.registration.showNotification(notification.title, { - icon: ICON_PATH, - body: notification.body, - image: notification.image, - tag: notification.link ?? DEFAULT_LINK, - }) + if (!notification) { + return } + + self.registration.showNotification(notification.title, { + icon: ICON_PATH, + body: notification.body, + image: notification.image, + tag: notification.link ?? DEFAULT_LINK, + }) + + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: isWebhookEvent(payload.data) ? payload.data.type : 'CUSTOM', + }) }) } diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index 6548c7f567..3ea350e1b8 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -15,7 +15,7 @@ import SafeLogo from '@/public/images/logo.svg' import Link from 'next/link' import useSafeAddress from '@/hooks/useSafeAddress' import BatchIndicator from '@/components/batch/BatchIndicator' -import { NotificationBanner } from '@/components/settings/Notifications/NotificationBanner' +import { PushNotificationsBanner } from '@/components/settings/PushNotifications/PushNotificationsBanner' type HeaderProps = { onMenuToggle?: Dispatch> @@ -74,9 +74,9 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { )}
- + - +
diff --git a/src/components/settings/Notifications/GlobalNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx similarity index 97% rename from src/components/settings/Notifications/GlobalNotifications.tsx rename to src/components/settings/PushNotifications/GlobalPushNotifications.tsx index fb3ec372e1..c49555284c 100644 --- a/src/components/settings/Notifications/GlobalNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -22,6 +22,8 @@ import CheckWallet from '@/components/common/CheckWallet' import { useNotificationPreferences } from './hooks/useNotificationPreferences' import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' import { selectAllAddedSafes } from '@/store/addedSafesSlice' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { requestNotificationPermission } from './logic' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' @@ -74,7 +76,7 @@ const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions? return notifiableSafes } -export const GlobalNotifications = (): ReactElement | null => { +export const GlobalPushNotifications = (): ReactElement | null => { const chains = useChains() const addedSafes = useAppSelector(selectAllAddedSafes) @@ -211,6 +213,8 @@ export const GlobalNotifications = (): ReactElement | null => { } Promise.all(registrationPromises) + + trackEvent(PUSH_NOTIFICATION_EVENTS.SAVE_SETTINGS) } if (totalNotifiableSafes === 0) { diff --git a/src/components/settings/Notifications/NotificationBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx similarity index 82% rename from src/components/settings/Notifications/NotificationBanner/index.tsx rename to src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index fe47fc84f8..3c67d0ba04 100644 --- a/src/components/settings/Notifications/NotificationBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -11,13 +11,15 @@ import { selectAllAddedSafes, selectTotalAdded } from '@/store/addedSafesSlice' import PushNotificationIcon from '@/public/images/notifications/push-notification.svg' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useNotificationRegistrations } from '../hooks/useNotificationRegistrations' -import { transformAddedSafes } from '../GlobalNotifications' +import { transformAddedSafes } from '../GlobalPushNotifications' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import { trackEvent } from '@/services/analytics' import css from './styles.module.css' const LS_KEY = 'dismissPushNotifications' -export const NotificationBanner = ({ children }: { children: ReactElement }): ReactElement => { +export const PushNotificationsBanner = ({ children }: { children: ReactElement }): ReactElement => { const [dismissedBanner = false, setDismissedBanner] = useLocalStorage(LS_KEY) const addedSafes = useAppSelector(selectAllAddedSafes) const totalAddedSafes = useAppSelector(selectTotalAdded) @@ -28,6 +30,8 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re const { registerNotifications } = useNotificationRegistrations() const dismissBanner = useCallback(() => { + trackEvent(PUSH_NOTIFICATION_EVENTS.DISMISS_BANNER) + setDismissedBanner(true) }, [setDismissedBanner]) @@ -37,6 +41,8 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re return } + trackEvent(PUSH_NOTIFICATION_EVENTS.DISPLAY_BANNER) + document.addEventListener('click', dismissBanner) return () => { document.removeEventListener('click', dismissBanner) @@ -44,10 +50,18 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re }, [dismissBanner, dismissedBanner]) const onEnableAll = () => { + trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL) + const safesToRegister = transformAddedSafes(addedSafes) registerNotifications(safesToRegister) - setDismissedBanner(true) + dismissBanner() + } + + const onCustomize = () => { + trackEvent(PUSH_NOTIFICATION_EVENTS.CUSTOMIZE_SETTINGS) + + dismissBanner() } if (dismissedBanner) { @@ -80,7 +94,7 @@ export const NotificationBanner = ({ children }: { children: ReactElement }): Re )} {safe && ( - + diff --git a/src/components/settings/Notifications/NotificationBanner/styles.module.css b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css similarity index 100% rename from src/components/settings/Notifications/NotificationBanner/styles.module.css rename to src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css diff --git a/src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts similarity index 100% rename from src/components/settings/Notifications/hooks/__tests__/useNotificationPreferences.test.ts rename to src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts diff --git a/src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts similarity index 100% rename from src/components/settings/Notifications/hooks/__tests__/useNotificationRegistrations.test.ts rename to src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts diff --git a/src/components/settings/Notifications/hooks/notifications-idb.ts b/src/components/settings/PushNotifications/hooks/notifications-idb.ts similarity index 100% rename from src/components/settings/Notifications/hooks/notifications-idb.ts rename to src/components/settings/PushNotifications/hooks/notifications-idb.ts diff --git a/src/components/settings/Notifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts similarity index 100% rename from src/components/settings/Notifications/hooks/useNotificationPreferences.ts rename to src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts diff --git a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts similarity index 79% rename from src/components/settings/Notifications/hooks/useNotificationRegistrations.ts rename to src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index 8a0d508c8c..a15f47111e 100644 --- a/src/components/settings/Notifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -4,6 +4,8 @@ import { useWeb3 } from '@/hooks/wallets/web3' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' import { useNotificationPreferences } from './useNotificationPreferences' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { getRegisterDevicePayload } from '../logic' import type { NotifiableSafes } from '../logic' @@ -42,14 +44,18 @@ export const useNotificationRegistrations = () => { _createPreferences(safesToRegister) - if (!withSignature) { - const isMultiple = - Object.keys(safesToRegister).length > 1 || Object.values(safesToRegister).some((safes) => safes.length > 1) + const totalRegistered = Object.values(safesToRegister).reduce((acc, safeAddresses) => acc + safeAddresses.length, 0) + + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.REGISTER_SAFES, + label: totalRegistered, + }) + if (!withSignature) { dispatch( showNotification({ message: `You will now receive notifications for ${ - isMultiple ? 'these Safe Accounts' : 'this Safe Account' + totalRegistered > 1 ? 'these Safe Accounts' : 'this Safe Account' } in your browser.`, variant: 'success', groupKey: 'notifications', @@ -73,9 +79,13 @@ export const useNotificationRegistrations = () => { console.error(`Error unregistering ${safeAddress} on chain ${chainId}`, e) } - if (didUnregister) { - _deletePreferences({ [chainId]: [safeAddress] }) + if (!didUnregister) { + return } + + _deletePreferences({ [chainId]: [safeAddress] }) + + trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) } const unregisterAllNotifications = async () => { @@ -96,9 +106,13 @@ export const useNotificationRegistrations = () => { console.error(`Error unregistering device on chain ${CHAIN_ID}`, e) } - if (didUnregister) { - _clearPreferences() + if (!didUnregister) { + return } + + _clearPreferences() + + trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE) } return { diff --git a/src/components/settings/Notifications/index.tsx b/src/components/settings/PushNotifications/index.tsx similarity index 88% rename from src/components/settings/Notifications/index.tsx rename to src/components/settings/PushNotifications/index.tsx index dc5e6dbdf2..a785a8ace7 100644 --- a/src/components/settings/Notifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -6,15 +6,17 @@ import EthHashInfo from '@/components/common/EthHashInfo' import { WebhookType } from '@/services/firebase/webhooks' import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' import { useNotificationPreferences } from './hooks/useNotificationPreferences' -import { GlobalNotifications } from './GlobalNotifications' +import { GlobalPushNotifications } from './GlobalPushNotifications' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { IS_DEV } from '@/config/constants' import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import css from './styles.module.css' -export const SafeNotifications = (): ReactElement => { +export const PushNotifications = (): ReactElement => { const dispatch = useAppDispatch() const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() @@ -34,8 +36,10 @@ export const SafeNotifications = (): ReactElement => { const handleOnChange = () => { if (preferences) { unregisterSafeNotifications(safe.chainId, safe.address.value) + trackEvent(PUSH_NOTIFICATION_EVENTS.DISABLE_SAFE) } else { registerNotifications({ [safe.chainId]: [safe.address.value] }) + trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_SAFE) } } @@ -87,7 +91,7 @@ export const SafeNotifications = (): ReactElement => {
) : ( - + )}
@@ -114,6 +118,8 @@ export const SafeNotifications = (): ReactElement => { [WebhookType.INCOMING_ETHER]: checked, [WebhookType.INCOMING_TOKEN]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_INCOMING_ASSETS, label: checked }) }} /> } @@ -130,6 +136,8 @@ export const SafeNotifications = (): ReactElement => { [WebhookType.OUTGOING_ETHER]: checked, [WebhookType.OUTGOING_TOKEN]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_OUTGOING_ASSETS, label: checked }) }} /> } @@ -144,6 +152,8 @@ export const SafeNotifications = (): ReactElement => { ...preferences, [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_PENDING_MULTISIG, label: checked }) }} /> } @@ -159,6 +169,8 @@ export const SafeNotifications = (): ReactElement => { ...preferences, [WebhookType.NEW_CONFIRMATION]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_NEW_CONFIRMATION, label: checked }) }} /> } @@ -174,6 +186,8 @@ export const SafeNotifications = (): ReactElement => { ...preferences, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_EXECUTED_MULTISIG, label: checked }) }} /> } @@ -189,6 +203,8 @@ export const SafeNotifications = (): ReactElement => { ...preferences, [WebhookType.MODULE_TRANSACTION]: checked, }) + + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_MODULE_TRANSACTION, label: checked }) }} /> } @@ -212,6 +228,8 @@ export const SafeNotifications = (): ReactElement => { [WebhookType.CONFIRMATION_REQUEST]: checked, }) + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_CONFIRMATION_REQUEST, label: checked }) + dispatch( showNotification({ message: diff --git a/src/components/settings/Notifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts similarity index 100% rename from src/components/settings/Notifications/logic.test.ts rename to src/components/settings/PushNotifications/logic.test.ts diff --git a/src/components/settings/Notifications/logic.ts b/src/components/settings/PushNotifications/logic.ts similarity index 93% rename from src/components/settings/Notifications/logic.ts rename to src/components/settings/PushNotifications/logic.ts index 28ce0664ed..d6f63a0ca4 100644 --- a/src/components/settings/Notifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -5,6 +5,8 @@ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typ import type { Web3Provider } from '@ethersproject/providers' import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/config/constants' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' type WithRequired = T & { [P in K]-?: T[P] } @@ -27,6 +29,8 @@ export const requestNotificationPermission = async (): Promise => { const isGranted = permission === 'granted' + trackEvent(isGranted ? PUSH_NOTIFICATION_EVENTS.GRANT_PERMISSION : PUSH_NOTIFICATION_EVENTS.REJECT_PERMISSION) + if (!isGranted) { alert('You must allow notifications to register your device.') } diff --git a/src/components/settings/Notifications/styles.module.css b/src/components/settings/PushNotifications/styles.module.css similarity index 100% rename from src/components/settings/Notifications/styles.module.css rename to src/components/settings/PushNotifications/styles.module.css diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx index 6793caab24..21d1059b9f 100644 --- a/src/pages/settings/notifications.tsx +++ b/src/pages/settings/notifications.tsx @@ -2,7 +2,7 @@ import Head from 'next/head' import type { NextPage } from 'next' import SettingsHeader from '@/components/settings/SettingsHeader' -import { SafeNotifications } from '@/components/settings/Notifications' +import { PushNotifications } from '@/components/settings/PushNotifications' const NotificationsPage: NextPage = () => { return ( @@ -14,7 +14,7 @@ const NotificationsPage: NextPage = () => {
- +
) diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts new file mode 100644 index 0000000000..fbf8d6d006 --- /dev/null +++ b/src/services/analytics/events/push-notifications.ts @@ -0,0 +1,109 @@ +export const category = 'push-notifications' + +export const PUSH_NOTIFICATION_EVENTS = { + // Browser notification shown to user + SHOW_NOTIFICATION: { + action: 'Show notification', + category, + }, + // User clicked on notification + CLICK_NOTIFICATION: { + action: 'Click notification', + category, + }, + // User granted notification permissions + GRANT_PERMISSION: { + action: 'Allow notifications', + category, + }, + // User refused notification permissions + REJECT_PERMISSION: { + action: 'Reject notifications', + category, + }, + // User registered Safe(s) for notifications + REGISTER_SAFES: { + action: 'Register Safe(s) notifications', + category, + }, + // User unregistered Safe from notifications + UNREGISTER_SAFE: { + action: 'Unregister Safe notifications', + category, + }, + // User unregistered device from notifications + UNREGISTER_DEVICE: { + action: 'Unregister device notifications', + category, + }, + // Notification banner displayed + DISPLAY_BANNER: { + action: 'Display notification banner', + category, + }, + // User dismissed notfication banner + DISMISS_BANNER: { + action: 'Dismiss notification banner', + category, + }, + // User enabled all notifications from banner + ENABLE_ALL: { + action: 'Enable all notifications', + category, + }, + // User opened Safe notification settings from banner + CUSTOMIZE_SETTINGS: { + action: 'Customize notifications', + category, + }, + // User turned notifications on for a Safe from settings + ENABLE_SAFE: { + action: 'Turn notifications on', + category, + }, + // User turned notifications off for a Safe from settings + DISABLE_SAFE: { + action: 'Turn notifications off', + category, + }, + // Save button clicked in global notification settings + SAVE_SETTINGS: { + action: 'Save notification settings', + category, + }, + // User changed the incoming assets notifications setting + TOGGLE_INCOMING_ASSETS: { + action: 'Toggle incoming assets notifications', + category, + }, + // User changed the outgoing assets notifications setting + TOGGLE_OUTGOING_ASSETS: { + action: 'Toggle outgoing assets notifications', + category, + }, + // User changed the pending multisig notifications setting + TOGGLE_PENDING_MULTISIG: { + action: 'Toggle pending multisig notifications', + category, + }, + // User changed the new confirmation notifications setting + TOGGLE_NEW_CONFIRMATION: { + action: 'Toggle new confirmation notifications', + category, + }, + // User changed the executed multisig transaction notifications setting + TOGGLE_EXECUTED_MULTISIG: { + action: 'Toggle executed multisig notifications', + category, + }, + // User changed the module transaction notifications setting + TOGGLE_MODULE_TRANSACTION: { + action: 'Toggle module transaction notifications', + category, + }, + // User changed the confirmation request notifications setting + TOGGLE_CONFIRMATION_REQUEST: { + action: 'Toggle confirmation request notifications', + category, + }, +} diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 7b6240741e..b79a28433b 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -11,11 +11,11 @@ import { GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/co import { createPreferencesStore, getSafeNotificationKey, -} from '@/components/settings/Notifications/hooks/notifications-idb' +} from '@/components/settings/PushNotifications/hooks/notifications-idb' import type { NotificationPreferences, SafeNotificationKey, -} from '@/components/settings/Notifications/hooks/notifications-idb' +} from '@/components/settings/PushNotifications/hooks/notifications-idb' export const shouldShowNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { From 82ceaf842e9002183b78ae7f58cdbf4a6ad3c064 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 14:48:17 +0200 Subject: [PATCH 24/62] fix: test --- src/services/firebase/index.test.ts | 10 +++++----- src/services/firebase/index.ts | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/services/firebase/index.test.ts b/src/services/firebase/index.test.ts index c9f719abd8..9fcabccddb 100644 --- a/src/services/firebase/index.test.ts +++ b/src/services/firebase/index.test.ts @@ -50,7 +50,7 @@ describe('parseWebhookNotification', () => { expect(notification).toEqual({ title: 'New confirmation', body: 'Safe 0x0000...0001 on Mainnet has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) @@ -92,7 +92,7 @@ describe('parseWebhookNotification', () => { expect(notification).toEqual({ title: 'Transaction executed', body: 'Safe 0x0000...0001 on Mainnet executed transaction 0x0000...0004.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) @@ -128,7 +128,7 @@ describe('parseWebhookNotification', () => { expect(notification).toEqual({ title: 'Transaction failed', body: 'Safe 0x0000...0001 on Mainnet failed to execute transaction 0x0000...0004.', - link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) @@ -169,7 +169,7 @@ describe('parseWebhookNotification', () => { expect(notification).toEqual({ title: 'New pending transaction', body: 'Safe 0x0000...0001 on Mainnet has a new pending transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) @@ -772,7 +772,7 @@ describe('parseWebhookNotification', () => { expect(notification).toEqual({ title: 'Confirmation request', body: 'Safe 0x0000...0001 on Mainnet has a new confirmation request for transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/queue?safe=eth:0x0000000000000000000000000000000000000001', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index b79a28433b..5f473ef9ca 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -61,14 +61,20 @@ const getBalances = async (chainId: string, safeAddress: string): Promise { +const getLink = (path: string, query: { address: string; chain?: ChainInfo; safeTxHash?: string }) => { const APP_URL = 'https://app.safe.global' - if (!chain) { + if (!query.chain) { return APP_URL } - return `${APP_URL}${path}?safe=${chain.shortName}:${address}` + const link = `${APP_URL}${path}?safe=${query.chain.shortName}:${query.address}` + + if (!query.safeTxHash) { + return link + } + + return `${link}&id=${query.safeTxHash}` } export const _parseWebhookNotification = async ( @@ -88,12 +94,9 @@ export const _parseWebhookNotification = async ( const shortSafeAddress = shortenAddress(address) - const historyLink = getLink(AppRoutes.transactions.history, address, chain) + const historyLink = getLink(AppRoutes.transactions.history, { address, chain }) - const getSafeTxHashLink = (safeTxHash: string): string => { - const txLink = getLink(AppRoutes.transactions.tx, address, chain) - return `${txLink}&id=${safeTxHash}` - } + const getSafeTxHashLink = (safeTxHash: string) => getLink(AppRoutes.transactions.tx, { address, chain, safeTxHash }) if (type === WebhookType.NEW_CONFIRMATION) { const { owner, safeTxHash } = data From e638ed485d580c8a14de0f9a0c39957202142079 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 25 Aug 2023 14:56:02 +0200 Subject: [PATCH 25/62] fix: condense preferences --- .../settings/PushNotifications/index.tsx | 67 ++++--------------- .../analytics/events/push-notifications.ts | 27 ++------ 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index a785a8ace7..c88fa55f2d 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -119,45 +119,38 @@ export const PushNotifications = (): ReactElement => { [WebhookType.INCOMING_TOKEN]: checked, }) - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_INCOMING_ASSETS, label: checked }) + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_INCOMING_TXS, label: checked }) }} /> } - label="Incoming assets" + label="Incoming transactions" /> { setPreferences({ ...preferences, [WebhookType.OUTGOING_ETHER]: checked, [WebhookType.OUTGOING_TOKEN]: checked, - }) - - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_OUTGOING_ASSETS, label: checked }) - }} - /> - } - label="Outgoing assets" - /> - { - setPreferences({ - ...preferences, + [WebhookType.MODULE_TRANSACTION]: checked, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, }) - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_PENDING_MULTISIG, label: checked }) + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_OUTGOING_TXS, label: checked }) }} /> } - label="Pending transactions" + label="Outgoing transactions" /> { label="New confirmations" /> - { - setPreferences({ - ...preferences, - [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, - }) - - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_EXECUTED_MULTISIG, label: checked }) - }} - /> - } - label="Executed transactions" - /> - - { - setPreferences({ - ...preferences, - [WebhookType.MODULE_TRANSACTION]: checked, - }) - - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_MODULE_TRANSACTION, label: checked }) - }} - /> - } - label="Module transactions" - /> - Date: Wed, 30 Aug 2023 13:08:30 +0200 Subject: [PATCH 26/62] fix: address review comments + adjust design --- public/firebase-messaging-sw.ts | 13 ++-- .../GlobalPushNotifications.tsx | 62 ++++++++--------- .../PushNotificationsBanner/index.tsx | 66 +++++++++---------- .../PushNotificationsBanner/styles.module.css | 27 ++------ .../useNotificationRegistrations.test.ts | 10 +-- .../hooks/useNotificationRegistrations.ts | 11 ++-- .../settings/PushNotifications/index.tsx | 27 +++++++- .../PushNotifications/styles.module.css | 13 +++- src/hooks/useFirebaseNotifications.ts | 10 ++- src/services/firebase/index.ts | 27 ++++++-- 10 files changed, 144 insertions(+), 122 deletions(-) diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 903dfb9096..7a999d1a34 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -1,11 +1,9 @@ /// -import { initializeApp } from 'firebase/app' import { onBackgroundMessage } from 'firebase/messaging/sw' import { getMessaging } from 'firebase/messaging/sw' -import { parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' -import { FIREBASE_OPTIONS } from '@/config/constants' +import { initializeFirebase, parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' import { trackEvent } from '@/services/analytics' import { isWebhookEvent } from '@/services/firebase/webhooks' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' @@ -17,9 +15,9 @@ declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown } // Satisfy Workbox self.__WB_MANIFEST -const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) +const app = initializeFirebase() -if (hasFirebaseOptions) { +if (app) { // Must be called before `onBackgroundMessage` as Firebase embeds a `notificationclick` listener self.addEventListener( 'notificationclick', @@ -39,13 +37,10 @@ if (hasFirebaseOptions) { false, ) - const app = initializeApp(FIREBASE_OPTIONS) - const messaging = getMessaging(app) onBackgroundMessage(messaging, async (payload) => { const ICON_PATH = '/images/safe-logo-green.png' - const DEFAULT_LINK = 'https://app.safe.global' const shouldShow = await shouldShowNotification(payload) @@ -63,7 +58,7 @@ if (hasFirebaseOptions) { icon: ICON_PATH, body: notification.body, image: notification.image, - tag: notification.link ?? DEFAULT_LINK, + tag: notification.link ?? self.location.origin, }) trackEvent({ diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index c49555284c..8bdec9773d 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -18,7 +18,6 @@ import EthHashInfo from '@/components/common/EthHashInfo' import { sameAddress } from '@/utils/addresses' import useChains from '@/hooks/useChains' import { useAppSelector } from '@/store' -import CheckWallet from '@/components/common/CheckWallet' import { useNotificationPreferences } from './hooks/useNotificationPreferences' import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' import { selectAllAddedSafes } from '@/store/addedSafesSlice' @@ -43,7 +42,7 @@ export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafe } // Convert data structure of currently notified Safes -const transformCurrentNotifiedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { +const transformCurrentSubscribedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { const obj: NotifiableSafes = {} for (const { chainId, safeAddress } of Object.values(allPreferences)) { @@ -81,7 +80,8 @@ export const GlobalPushNotifications = (): ReactElement | null => { const addedSafes = useAppSelector(selectAllAddedSafes) const { getAllPreferences } = useNotificationPreferences() - const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() + const { unregisterChainNotifications, unregisterSafeNotifications, registerNotifications } = + useNotificationRegistrations() // Safes selected in the UI const [selectedSafes, setSelectedSafes] = useState({}) @@ -92,7 +92,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { if (!allPreferences) { return } - return transformCurrentNotifiedSafes(allPreferences) + return transformCurrentSubscribedSafes(allPreferences) }, [getAllPreferences]) // `currentNotifiedSafes` is initially undefined until indexedDB resolves @@ -140,18 +140,18 @@ export const GlobalPushNotifications = (): ReactElement | null => { }) } - // Wether Safes need to be (un-)reigstered with the service - const shouldRegisterSafes = Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { + // 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)) }) - const shouldUnregisterSafes = + const shouldUnregisterUnselectedSafes = currentNotifiedSafes && Object.entries(currentNotifiedSafes).some(([chainId, safeAddresses]) => { return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) }) - const canSave = shouldRegisterSafes || shouldUnregisterSafes + const canSave = shouldRegisterSelectedSafes || shouldUnregisterUnselectedSafes const onSave = async () => { if (!canSave) { @@ -164,13 +164,6 @@ export const GlobalPushNotifications = (): ReactElement | null => { return } - // TODO: Enable when live on prod. - // const shouldUnregisterDevice = Object.values(selectedSafes).every((safeAddresses) => safeAddresses.length === 0) - // if (shouldUnregisterDevice) { - // unregisterAllNotifications() - // return - // } - const registrationPromises: Array> = [] const safesToRegister = Object.entries(selectedSafes).reduce((acc, [chainId, safeAddresses]) => { @@ -185,7 +178,13 @@ export const GlobalPushNotifications = (): ReactElement | null => { return acc }, {}) - const safesToUnregister = + const shouldRegister = Object.values(safesToRegister).some((safeAddresses) => safeAddresses.length > 0) + + if (shouldRegister) { + registrationPromises.push(registerNotifications(safesToRegister)) + } + + const shouldUnregister = currentNotifiedSafes && Object.entries(currentNotifiedSafes).reduce((acc, [chainId, safeAddresses]) => { const safesToUnregisterOnChain = safeAddresses.filter( @@ -198,21 +197,26 @@ export const GlobalPushNotifications = (): ReactElement | null => { return acc }, {}) - const shouldRegisterSafes = Object.values(safesToRegister).some((safeAddresses) => safeAddresses.length > 0) - - if (shouldRegisterSafes) { - registrationPromises.push(registerNotifications(safesToRegister)) - } + if (shouldUnregister) { + for (const [chainId, safeAddresses] of Object.entries(shouldUnregister)) { + const shouldUnregsiterDevice = + safeAddresses.length === currentNotifiedSafes[chainId].length && + safeAddresses.every((safeAddress) => { + return currentNotifiedSafes[chainId]?.includes(safeAddress) + }) + + if (shouldUnregsiterDevice) { + registrationPromises.push(unregisterChainNotifications(chainId)) + continue + } - if (safesToUnregister) { - for (const [chainId, safeAddresses] of Object.entries(safesToUnregister)) { for (const safeAddress of safeAddresses) { registrationPromises.push(unregisterSafeNotifications(chainId, safeAddress)) } } } - Promise.all(registrationPromises) + await Promise.all(registrationPromises) trackEvent(PUSH_NOTIFICATION_EVENTS.SAVE_SETTINGS) } @@ -228,13 +232,9 @@ export const GlobalPushNotifications = (): ReactElement | null => { My Safes ({totalNotifiableSafes}) - - {(isOk) => ( - - )} - +
diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index 122f183c18..87a4f1622f 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -1,4 +1,4 @@ -import { Button, Chip, SvgIcon, Typography } from '@mui/material' +import { Button, Chip, Grid, SvgIcon, Typography } from '@mui/material' import Link from 'next/link' import { useRouter } from 'next/router' import { useCallback, useEffect } from 'react' @@ -72,41 +72,39 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } -
-
-
- - - Enable push notifications - -
- - Easily track your Safe Account activity with broswer push notifications. - -
+ + + -
-
- {totalAddedSafes > 0 && ( - - )} - {safe && ( - - - - )} -
- + )} + {safe && ( + + + + )} + +
+
} open > diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css index a218c3ec50..4e3a5a99da 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css @@ -17,29 +17,24 @@ min-width: 100%; } -.content { - display: flex; - align-items: center; -} - -.text { - display: flex; - flex-direction: column; +.button { + padding: 4px 10px; } -.title { +.buttons { display: flex; - align-items: center; + gap: var(--space-2); } .chip { border-radius: 4px; background-color: var(--color-secondary-main); - margin-right: var(--space-1); font-weight: 400; font-size: 12px; width: var(--space-5); height: 24px; + z-index: 9999999; + position: relative; } [data-theme='dark'] .chip { @@ -53,15 +48,7 @@ } .icon { + margin-top: -12px; width: 64px; height: 64px; } - -.buttons { - display: flex; - gap: var(--space-2); -} - -.button { - padding: 4px 10px; -} diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index 2e64e4fe44..2cfc4520d0 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -344,7 +344,7 @@ describe('useNotificationRegistrations', () => { }) }) - describe('unregisterAllNotifications', () => { + describe('unregisterChainNotifications', () => { const unregisterDeviceSpy = jest.spyOn(sdk, 'unregisterDevice') it('does not unregister device if no uuid is present', async () => { @@ -357,7 +357,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterAllNotifications() + await result.current.unregisterChainNotifications('1') expect(unregisterDeviceSpy).not.toHaveBeenCalled() }) @@ -379,7 +379,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterAllNotifications() + await result.current.unregisterChainNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) @@ -402,7 +402,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterAllNotifications() + await result.current.unregisterChainNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) @@ -425,7 +425,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterAllNotifications() + await result.current.unregisterChainNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index a15f47111e..86d12d80a8 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -88,22 +88,19 @@ export const useNotificationRegistrations = () => { trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) } - const unregisterAllNotifications = async () => { + const unregisterChainNotifications = async (chainId: string) => { if (!uuid) { return } - // Device unregistration is chain agnostic but is required for the route - const CHAIN_ID = '1' - let didUnregister = false try { - const response = await unregisterDevice(CHAIN_ID, uuid) + const response = await unregisterDevice(chainId, uuid) didUnregister = response == null } catch (e) { - console.error(`Error unregistering device on chain ${CHAIN_ID}`, e) + console.error(`Error unregistering device on chain ${chainId}`, e) } if (!didUnregister) { @@ -118,6 +115,6 @@ export const useNotificationRegistrations = () => { return { registerNotifications, unregisterSafeNotifications, - unregisterAllNotifications, + unregisterChainNotifications, } } diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index c88fa55f2d..2dd01e8ef1 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -1,4 +1,16 @@ -import { Grid, Paper, Typography, Checkbox, FormControlLabel, FormGroup, Alert, Switch, Divider } from '@mui/material' +import { + Grid, + Paper, + Typography, + Checkbox, + FormControlLabel, + FormGroup, + Alert, + Switch, + Divider, + Link as MuiLink, +} from '@mui/material' +import Link from 'next/link' import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' @@ -13,6 +25,7 @@ import { useAppDispatch } from '@/store' import { showNotification } from '@/store/notificationsSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import { AppRoutes } from '@/config/routes' import css from './styles.module.css' @@ -61,7 +74,7 @@ export const PushNotifications = (): ReactElement => { {shouldShowMacHelper && ( - + For MacOS users @@ -89,6 +102,16 @@ export const PushNotifications = (): ReactElement => { label={preferences ? 'On' : 'Off'} /> + + + + Want to setup notifications for different or all Safes? You can do so in your{' '} + + global preferences + + . + + ) : ( diff --git a/src/components/settings/PushNotifications/styles.module.css b/src/components/settings/PushNotifications/styles.module.css index 1a42be0c9a..963f986c7a 100644 --- a/src/components/settings/PushNotifications/styles.module.css +++ b/src/components/settings/PushNotifications/styles.module.css @@ -1,15 +1,15 @@ -.info { +.macOsInfo { border-color: var(--color-border-light); background-color: var(--color-background-main); padding: var(--space-2); } -.info :global .MuiAlert-icon { +.macOsInfo :global .MuiAlert-icon { color: var(--color-text-main); padding: 0; } -.info :global .MuiAlert-message { +.macOsInfo :global .MuiAlert-message { padding: 0; } @@ -20,3 +20,10 @@ .icon { min-width: 38px; } + +.globalInfo { + border-radius: 6px; + border: 1px solid var(--color-secondary-light); + background-color: var(--color-secondary-background); + padding: var(--space-2); +} diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index ad629367ed..b6c9bee28d 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' -import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_OPTIONS } from '@/config/constants' -import { initializeApp } from 'firebase/app' +import { initializeFirebase } from '@/services/firebase' +import { FIREBASE_MESSAGING_SW_PATH } from '@/config/constants' export const useFirebaseNotifications = (): void => { // Register servicer worker @@ -10,14 +10,12 @@ export const useFirebaseNotifications = (): void => { return } - const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) + const app = initializeFirebase() - if (!hasFirebaseOptions) { + if (!app) { return } - initializeApp(FIREBASE_OPTIONS) - const registerFirebaseSw = () => { navigator.serviceWorker.register(FIREBASE_MESSAGING_SW_PATH).catch(() => null) } diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 5f473ef9ca..954a2bf378 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -1,5 +1,6 @@ import { formatUnits } from 'ethers/lib/utils' import { get } from 'idb-keyval' +import { initializeApp } from 'firebase/app' import type { MessagePayload } from 'firebase/messaging/sw' import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' @@ -7,7 +8,7 @@ import { shortenAddress } from '@/utils/formatters' import { AppRoutes } from '@/config/routes' import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' import type { WebhookEvent } from '@/services/firebase/webhooks' -import { GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' +import { FIREBASE_OPTIONS, GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' import { createPreferencesStore, getSafeNotificationKey, @@ -17,6 +18,24 @@ import type { SafeNotificationKey, } from '@/components/settings/PushNotifications/hooks/notifications-idb' +export const initializeFirebase = () => { + const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) + + if (!hasFirebaseOptions) { + return + } + + let app: ReturnType | null = null + + try { + app = initializeApp(FIREBASE_OPTIONS) + } catch (e) { + console.error('[Firebase] Initialization failed', e) + } + + return app +} + export const shouldShowNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { return true @@ -62,13 +81,11 @@ const getBalances = async (chainId: string, safeAddress: string): Promise { - const APP_URL = 'https://app.safe.global' - if (!query.chain) { - return APP_URL + return self.location.origin } - const link = `${APP_URL}${path}?safe=${query.chain.shortName}:${query.address}` + const link = `${self.location.origin}${path}?safe=${query.chain.shortName}:${query.address}` if (!query.safeTxHash) { return link From e861cedf87cac229462cdf69722e29e7df779f5f Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 30 Aug 2023 13:20:51 +0200 Subject: [PATCH 27/62] fix: test --- src/services/firebase/index.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/firebase/index.test.ts b/src/services/firebase/index.test.ts index 9fcabccddb..2b61af6cca 100644 --- a/src/services/firebase/index.test.ts +++ b/src/services/firebase/index.test.ts @@ -24,6 +24,12 @@ const setupFetchStub = (data: any) => (_url: string) => { }) } +Object.defineProperty(self, 'location', { + value: { + origin: 'https://app.safe.global', + }, +}) + describe('parseWebhookNotification', () => { beforeEach(() => { global.fetch = jest.fn() From d24661066a78008f7f359f4115cb45401db2f809 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 11:59:55 +0200 Subject: [PATCH 28/62] refactor: clean up code --- public/firebase-messaging-sw.ts | 14 +---- .../hooks/useNotificationPreferences.ts | 47 +++++++++++---- .../hooks/useNotificationRegistrations.ts | 60 ++++++++----------- .../settings/PushNotifications/logic.ts | 7 ++- src/services/firebase/index.ts | 14 +++-- 5 files changed, 79 insertions(+), 63 deletions(-) diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 7a999d1a34..52f7e5e359 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -4,9 +4,8 @@ import { onBackgroundMessage } from 'firebase/messaging/sw' import { getMessaging } from 'firebase/messaging/sw' import { initializeFirebase, parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' -import { trackEvent } from '@/services/analytics' -import { isWebhookEvent } from '@/services/firebase/webhooks' -import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' + +const ICON_PATH = '/images/safe-logo-green.png' // Default type of `self` is `WorkerGlobalScope & typeof globalThis` // https://github.com/microsoft/TypeScript/issues/14877 @@ -31,8 +30,6 @@ if (app) { } self.clients.openWindow(link) - - trackEvent(PUSH_NOTIFICATION_EVENTS.CLICK_NOTIFICATION) }, false, ) @@ -40,8 +37,6 @@ if (app) { const messaging = getMessaging(app) onBackgroundMessage(messaging, async (payload) => { - const ICON_PATH = '/images/safe-logo-green.png' - const shouldShow = await shouldShowNotification(payload) if (!shouldShow) { @@ -60,10 +55,5 @@ if (app) { image: notification.image, tag: notification.link ?? self.location.origin, }) - - trackEvent({ - ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, - label: isWebhookEvent(payload.data) ? payload.data.type : 'CUSTOM', - }) }) } diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index d49b225a7e..d501ee1174 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -1,10 +1,18 @@ -import { set, entries, delMany, setMany, clear, update } from 'idb-keyval' +import { + set as setIndexedDb, + entries as getEntriesFromIndexedDb, + delMany as deleteManyFromIndexedDb, + setMany as setManyIndexedDb, + clear as clearIndexedDb, + update as updateIndexedDb, +} from 'idb-keyval' import { useCallback, useEffect, useMemo } from 'react' import { WebhookType } from '@/services/firebase/webhooks' import ExternalStore from '@/services/ExternalStore' import { createPreferencesStore, createUuidStore, getSafeNotificationKey } from './notifications-idb' import type { NotificationPreferences, SafeNotificationKey } from './notifications-idb' +import type { NotifiableSafes } from '../logic' export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationKey]['preferences'] = { [WebhookType.NEW_CONFIRMATION]: true, @@ -27,7 +35,19 @@ const { useStore: usePreferences, setStore: setPreferences } = new ExternalStore export const _setUuid = setUuid export const _setPreferences = setPreferences -export const useNotificationPreferences = () => { +export const useNotificationPreferences = (): { + uuid: string | undefined + getAllPreferences: () => NotificationPreferences | undefined + getPreferences: (chainId: string, safeAddress: string) => typeof _DEFAULT_NOTIFICATION_PREFERENCES | undefined + updatePreferences: ( + chainId: string, + safeAddress: string, + preferences: NotificationPreferences[SafeNotificationKey]['preferences'], + ) => void + _createPreferences: (safesToRegister: NotifiableSafes, withConfirmationRequests?: boolean) => void + _deletePreferences: (safesToUnregister: NotifiableSafes) => void + _clearPreferences: () => void +} => { // State const uuid = useUuid() const preferences = usePreferences() @@ -65,7 +85,7 @@ export const useNotificationPreferences = () => { let _uuid: string - update( + updateIndexedDb( UUID_KEY, (storedUuid) => { // Initialise UUID if it doesn't exist @@ -91,7 +111,7 @@ export const useNotificationPreferences = () => { return } - entries(preferencesStore) + getEntriesFromIndexedDb(preferencesStore) .then((preferencesEntries) => { setPreferences(Object.fromEntries(preferencesEntries)) }) @@ -104,7 +124,10 @@ export const useNotificationPreferences = () => { }, [hydratePreferences]) // Add store entry with default preferences for specified Safe(s) - const createPreferences = (safesToRegister: { [chain: string]: Array }) => { + const createPreferences = ( + safesToRegister: { [chain: string]: Array }, + withConfirmationRequests?: boolean, + ) => { if (!preferencesStore) { return } @@ -116,14 +139,16 @@ export const useNotificationPreferences = () => { const defaultPreferences: NotificationPreferences[SafeNotificationKey] = { chainId, safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: withConfirmationRequests + ? { ..._DEFAULT_NOTIFICATION_PREFERENCES, [WebhookType.CONFIRMATION_REQUEST]: true } + : _DEFAULT_NOTIFICATION_PREFERENCES, } return [key, defaultPreferences] }) }) - setMany(defaultPreferencesEntries, preferencesStore) + setManyIndexedDb(defaultPreferencesEntries, preferencesStore) .then(hydratePreferences) .catch(() => null) } @@ -146,7 +171,7 @@ export const useNotificationPreferences = () => { preferences, } - set(key, newPreferences, preferencesStore) + setIndexedDb(key, newPreferences, preferencesStore) .then(hydratePreferences) .catch(() => null) } @@ -161,7 +186,7 @@ export const useNotificationPreferences = () => { return safeAddresses.map((safeAddress) => getSafeNotificationKey(chainId, safeAddress)) }) - delMany(keysToDelete, preferencesStore) + deleteManyFromIndexedDb(keysToDelete, preferencesStore) .then(hydratePreferences) .catch(() => null) } @@ -172,7 +197,7 @@ export const useNotificationPreferences = () => { return } - clear(preferencesStore) + clearIndexedDb(preferencesStore) .then(hydratePreferences) .catch(() => null) } @@ -181,8 +206,8 @@ export const useNotificationPreferences = () => { uuid, getAllPreferences, getPreferences, - _createPreferences: createPreferences, updatePreferences, + _createPreferences: createPreferences, _deletePreferences: deletePreferences, _clearPreferences: clearPreferences, } diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index 86d12d80a8..0edb2664ce 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -9,13 +9,17 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif import { getRegisterDevicePayload } from '../logic' import type { NotifiableSafes } from '../logic' -export const useNotificationRegistrations = () => { +export const useNotificationRegistrations = (): { + registerNotifications: (safesToRegister: NotifiableSafes, withSignature?: boolean) => Promise + unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise + unregisterChainNotifications: (chainId: string) => Promise +} => { const dispatch = useAppDispatch() const web3 = useWeb3() const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences() - const registerNotifications = async (safesToRegister: NotifiableSafes, withSignature = false) => { + const registerNotifications = async (safesToRegister: NotifiableSafes, withConfirmationRequests = false) => { if (!uuid) { return } @@ -26,7 +30,7 @@ export const useNotificationRegistrations = () => { const payload = await getRegisterDevicePayload({ uuid, safesToRegister, - web3: withSignature ? web3 : undefined, + web3: withConfirmationRequests ? web3 : undefined, }) // Gateway will return 200 with an empty payload if the device was registered successfully @@ -42,7 +46,7 @@ export const useNotificationRegistrations = () => { return } - _createPreferences(safesToRegister) + _createPreferences(safesToRegister, withConfirmationRequests) const totalRegistered = Object.values(safesToRegister).reduce((acc, safeAddresses) => acc + safeAddresses.length, 0) @@ -51,7 +55,7 @@ export const useNotificationRegistrations = () => { label: totalRegistered, }) - if (!withSignature) { + if (!withConfirmationRequests) { dispatch( showNotification({ message: `You will now receive notifications for ${ @@ -64,52 +68,40 @@ export const useNotificationRegistrations = () => { } } - const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { - if (!uuid) { - return - } - + const unregisterNotifications = async (unregistrationFn: Promise, callback: () => void) => { let didUnregister = false try { - const response = await unregisterSafe(chainId, safeAddress, uuid) + const response = await unregistrationFn didUnregister = response == null } catch (e) { - console.error(`Error unregistering ${safeAddress} on chain ${chainId}`, e) + console.error('Error unregistering', e) } if (!didUnregister) { return } - _deletePreferences({ [chainId]: [safeAddress] }) - - trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) + callback() } - const unregisterChainNotifications = async (chainId: string) => { - if (!uuid) { - return - } - - let didUnregister = false - - try { - const response = await unregisterDevice(chainId, uuid) - - didUnregister = response == null - } catch (e) { - console.error(`Error unregistering device on chain ${chainId}`, e) + const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { + if (uuid) { + await unregisterNotifications(unregisterSafe(chainId, safeAddress, uuid), () => { + _deletePreferences({ [chainId]: [safeAddress] }) + trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) + }) } + } - if (!didUnregister) { - return + const unregisterChainNotifications = async (chainId: string) => { + if (uuid) { + await unregisterNotifications(unregisterDevice(chainId, uuid), () => { + _clearPreferences() + trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE) + }) } - - _clearPreferences() - - trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE) } return { diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index d6f63a0ca4..22a9a8089d 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -70,6 +70,9 @@ export const getRegisterDevicePayload = async ({ uuid: string web3?: Web3Provider }): Promise => { + const BUILD_NUMBER = '0' // Required value, but does not exist on web + const BUNDLE = 'safe' + const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) // Get Firebase token @@ -108,8 +111,8 @@ export const getRegisterDevicePayload = async ({ return { uuid, cloudMessagingToken: token, - buildNumber: '0', // Required value, but does not exist on web - bundle: location.origin, + buildNumber: BUILD_NUMBER, + bundle: BUNDLE, deviceType: DeviceType.WEB, version: packageJson.version, timestamp, diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 954a2bf378..51f99b8e0c 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -1,5 +1,5 @@ import { formatUnits } from 'ethers/lib/utils' -import { get } from 'idb-keyval' +import { get as getFromIndexedDb } from 'idb-keyval' import { initializeApp } from 'firebase/app' import type { MessagePayload } from 'firebase/messaging/sw' import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' @@ -46,7 +46,9 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise(key, store).catch(() => null) + const preferencesStore = await getFromIndexedDb(key, store).catch( + () => null, + ) if (!preferencesStore) { return false @@ -59,6 +61,7 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise => { const ENDPOINT = `${BASE_URL}/v1/chains` @@ -119,7 +122,7 @@ export const _parseWebhookNotification = async ( const { owner, safeTxHash } = data return { - title: `New confirmation`, + title: `Transaction confirmation`, body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation from ${shortenAddress( owner, )} on transaction ${shortenAddress(safeTxHash)}.`, @@ -147,6 +150,8 @@ export const _parseWebhookNotification = async ( } } + // TODO: Check notification + // https://github.com/safe-global/safe-transaction-service/blob/5d648f7dea05d46c00a7d43f9585a067a685758b/safe_transaction_service/notifications/tasks.py#L63 if (type === WebhookType.PENDING_MULTISIG_TRANSACTION) { const { safeTxHash } = data @@ -263,7 +268,8 @@ export const parseFirebaseNotification = async ( } } - // Firebase-dispatched notification + // Manually dispatched notifications from the Firebase admin panel + // Displayed as is if (payload.notification) { return { title: payload.notification.title || '', From c2e366a18e786db7c5780ca04c4f3de0cb17c488 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 13:29:17 +0200 Subject: [PATCH 29/62] refactor: make `_parseWebhookNotification` a map --- src/services/firebase/index.test.ts | 12 +- src/services/firebase/index.ts | 279 ++++++++++++++-------------- 2 files changed, 145 insertions(+), 146 deletions(-) diff --git a/src/services/firebase/index.test.ts b/src/services/firebase/index.test.ts index 2b61af6cca..034f842a0f 100644 --- a/src/services/firebase/index.test.ts +++ b/src/services/firebase/index.test.ts @@ -54,7 +54,7 @@ describe('parseWebhookNotification', () => { const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ - title: 'New confirmation', + title: 'Transaction confirmation', body: 'Safe 0x0000...0001 on Mainnet has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) @@ -66,7 +66,7 @@ describe('parseWebhookNotification', () => { const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ - title: 'New confirmation', + title: 'Transaction confirmation', body: 'Safe 0x0000...0001 on chain 1 has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', link: 'https://app.safe.global', }) @@ -173,8 +173,8 @@ describe('parseWebhookNotification', () => { const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ - title: 'New pending transaction', - body: 'Safe 0x0000...0001 on Mainnet has a new pending transaction 0x0000...0003.', + title: 'Pending transaction', + body: 'Safe 0x0000...0001 on Mainnet has a pending transaction 0x0000...0003.', link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', }) }) @@ -185,8 +185,8 @@ describe('parseWebhookNotification', () => { const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ - title: 'New pending transaction', - body: 'Safe 0x0000...0001 on chain 1 has a new pending transaction 0x0000...0003.', + title: 'Pending transaction', + body: 'Safe 0x0000...0001 on chain 1 has a pending transaction 0x0000...0003.', link: 'https://app.safe.global', }) }) diff --git a/src/services/firebase/index.ts b/src/services/firebase/index.ts index 51f99b8e0c..51304ebc5b 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/index.ts @@ -62,194 +62,193 @@ const BASE_URL = IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING // XHR is not supported in service workers so we can't use the SDK // TODO: Migrate to SDK when we update it to use fetch -const getChains = async (): Promise => { +const getChain = async (chainId: string): Promise => { const ENDPOINT = `${BASE_URL}/v1/chains` - const response = await fetch(ENDPOINT) + let chains: ChainListResponse | null = null - if (response.ok) { - return response.json() - } + try { + const response = await fetch(ENDPOINT) + if (response.ok) { + chains = await response.json() + } + } catch {} + + return chains?.results.find((chain) => chain.chainId === chainId) } -const getBalances = async (chainId: string, safeAddress: string): Promise => { +const getTokenInfo = async ( + chainId: string, + safeAddress: string, + tokenAddress: string, + tokenValue?: string, +): Promise<{ symbol: string; value: string; name: string }> => { const DEFAULT_CURRENCY = 'USD' const ENDPOINT = `${BASE_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` - const response = await fetch(ENDPOINT) + const DEFAULT_INFO = { + symbol: 'tokens', + value: 'some', + name: 'Token', + } + + let balances: SafeBalanceResponse | null = null - if (response.ok) { - return response.json() + try { + const response = await fetch(ENDPOINT) + if (response.ok) { + balances = await response.json() + } + } catch {} + + const tokenInfo = balances?.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo + + if (!tokenInfo) { + return DEFAULT_INFO + } + + const symbol = tokenInfo?.symbol ?? DEFAULT_INFO.symbol + const value = tokenValue && tokenInfo ? formatUnits(tokenValue, tokenInfo.decimals).toString() : DEFAULT_INFO.value + const name = tokenInfo?.name ?? DEFAULT_INFO.name + + return { + symbol, + value, + name, } } -const getLink = (path: string, query: { address: string; chain?: ChainInfo; safeTxHash?: string }) => { - if (!query.chain) { - return self.location.origin +const getLink = (data: WebhookEvent, shortName?: string) => { + const URL = self.location.origin + + if (!shortName) { + return URL } - const link = `${self.location.origin}${path}?safe=${query.chain.shortName}:${query.address}` + const withRoute = (route: string) => { + return `${URL}${route}?safe=${shortName}:${data.address}` + } - if (!query.safeTxHash) { - return link + if ('safeTxHash' in data) { + return `${withRoute(AppRoutes.transactions.tx)}&id=${data.safeTxHash}` } - return `${link}&id=${query.safeTxHash}` + return withRoute(AppRoutes.transactions.history) +} + +type NotificationsMap = { + [P in T['type']]: ( + data: Extract, + ) => Promise<{ title: string; body: string }> | { title: string; body: string } | null } export const _parseWebhookNotification = async ( data: WebhookEvent, ): Promise<{ title: string; body: string; link: string } | undefined> => { - const { type, chainId, address } = data - - let chains: Array | undefined + let chain: ChainInfo | undefined try { - const response = await getChains() - chains = response?.results + chain = await getChain(data.chainId) } catch {} - const chain = chains?.find((chain) => chain.chainId === chainId) - const chainName = chain?.chainName ?? `chain ${chainId}` - - const shortSafeAddress = shortenAddress(address) - - const historyLink = getLink(AppRoutes.transactions.history, { address, chain }) + const chainName = chain?.chainName ?? `chain ${data.chainId}` - const getSafeTxHashLink = (safeTxHash: string) => getLink(AppRoutes.transactions.tx, { address, chain, safeTxHash }) + const currencySymbol = chain?.nativeCurrency?.symbol ?? 'ETH' + const currencyName = chain?.nativeCurrency?.name ?? 'Ether' - if (type === WebhookType.NEW_CONFIRMATION) { - const { owner, safeTxHash } = data - - return { - title: `Transaction confirmation`, - body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation from ${shortenAddress( - owner, - )} on transaction ${shortenAddress(safeTxHash)}.`, - link: getSafeTxHashLink(safeTxHash), - } - } - - if (type === WebhookType.EXECUTED_MULTISIG_TRANSACTION) { - const { failed, txHash, safeTxHash } = data - - const shortTxHash = shortenAddress(txHash) - - if (failed === 'true') { + const Notifications: NotificationsMap = { + [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash }) => { return { - title: `Transaction failed`, - body: `Safe ${shortSafeAddress} on ${chainName} failed to execute transaction ${shortTxHash}.`, - link: getSafeTxHashLink(safeTxHash), + title: 'Transaction confirmation', + body: `Safe ${shortenAddress(address)} on ${chainName} has a new confirmation from ${shortenAddress( + owner, + )} on transaction ${shortenAddress(safeTxHash)}.`, } - } else { + }, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash }) => { + const didFail = failed === 'true' return { - title: `Transaction executed`, - body: `Safe ${shortSafeAddress} on ${chainName} executed transaction ${shortTxHash}.`, - link: getSafeTxHashLink(safeTxHash), + title: `Transaction ${didFail ? 'failed' : 'executed'}`, + body: `Safe ${shortenAddress(address)} on ${chainName} ${ + didFail ? 'failed to execute' : 'executed' + } transaction ${shortenAddress(txHash)}.`, } - } - } - - // TODO: Check notification - // https://github.com/safe-global/safe-transaction-service/blob/5d648f7dea05d46c00a7d43f9585a067a685758b/safe_transaction_service/notifications/tasks.py#L63 - if (type === WebhookType.PENDING_MULTISIG_TRANSACTION) { - const { safeTxHash } = data - - return { - title: `New pending transaction`, - body: `Safe ${shortSafeAddress} on ${chainName} has a new pending transaction ${shortenAddress(safeTxHash)}.`, - link: getSafeTxHashLink(safeTxHash), - } - } - - if (type === WebhookType.INCOMING_ETHER || type === WebhookType.OUTGOING_ETHER) { - const { txHash, value } = data - - const currencySymbol = chain?.nativeCurrency?.symbol ?? 'ETH' - const currencyValue = formatUnits(value, chain?.nativeCurrency?.decimals).toString() - const currencyName = chain?.nativeCurrency?.name ?? 'Ether' - - const shortTxHash = shortenAddress(txHash) - - if (type === WebhookType.INCOMING_ETHER) { + }, + [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash }) => { + return { + title: 'Pending transaction', + body: `Safe ${shortenAddress(address)} on ${chainName} has a pending transaction ${shortenAddress( + safeTxHash, + )}.`, + } + }, + [WebhookType.INCOMING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} received`, - body: `Safe ${shortSafeAddress} on ${chainName} received ${currencyValue} ${currencySymbol} in transaction ${shortTxHash}.`, - link: historyLink, + body: `Safe ${shortenAddress(address)} on ${chainName} received ${formatUnits( + value, + chain?.nativeCurrency?.decimals, + ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, } - } - - if (type === WebhookType.OUTGOING_ETHER) { + }, + [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} sent`, - body: `Safe ${shortSafeAddress} on ${chainName} sent ${currencyValue} ${currencySymbol} in transaction ${shortTxHash}.`, - link: historyLink, + body: `Safe ${shortenAddress(address)} on ${chainName} sent ${formatUnits( + value, + chain?.nativeCurrency?.decimals, + ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, } - } - } - - if (type === WebhookType.INCOMING_TOKEN || type === WebhookType.OUTGOING_TOKEN) { - const { tokenAddress, txHash, value } = data - - let balances: SafeBalanceResponse | undefined - - try { - balances = await getBalances(chainId, address) - } catch {} - - const tokenInfo = balances?.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo - - const tokenSymbol = tokenInfo?.symbol ?? 'tokens' - const tokenValue = value && tokenInfo ? formatUnits(value, tokenInfo.decimals).toString() : 'some' - const tokenName = tokenInfo?.name ?? 'Token' - - const shortTxHash = shortenAddress(txHash) - - if (type === WebhookType.INCOMING_TOKEN) { + }, + [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { + const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { - title: `${tokenName} received`, - body: `Safe ${shortSafeAddress} on ${chainName} received ${tokenValue} ${tokenSymbol} in transaction ${shortTxHash}.`, - link: historyLink, + title: `${token.name} received`, + body: `Safe ${shortenAddress(address)} on ${chainName} received ${token.value} ${ + token.symbol + } in transaction ${shortenAddress(txHash)}.`, } - } - - if (type === WebhookType.OUTGOING_TOKEN) { + }, + [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { + const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { - title: `${tokenName} sent`, - body: `Safe ${shortSafeAddress} on ${chainName} sent ${tokenValue} ${tokenSymbol} in transaction ${shortTxHash}.`, - link: historyLink, + title: `${token.name} sent`, + body: `Safe ${shortenAddress(address)} on ${chainName} sent ${token.value} ${ + token.symbol + } in transaction ${shortenAddress(txHash)}.`, } - } - } - - if (type === WebhookType.MODULE_TRANSACTION) { - const { module, txHash } = data - - return { - title: `Module transaction`, - body: `Safe ${shortSafeAddress} on ${chainName} executed a module transaction ${shortenAddress( - txHash, - )} from module ${shortenAddress(module)}.`, - link: historyLink, - } + }, + [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash }) => { + return { + title: 'Module transaction', + body: `Safe ${shortenAddress(address)} on ${chainName} executed a module transaction ${shortenAddress( + txHash, + )} from module ${shortenAddress(module)}.`, + } + }, + [WebhookType.CONFIRMATION_REQUEST]: ({ address, safeTxHash }) => { + return { + title: 'Confirmation request', + body: `Safe ${shortenAddress( + address, + )} on ${chainName} has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.`, + } + }, + [WebhookType.SAFE_CREATED]: () => { + // We do not preemptively subscribe to Safes before they are created + return null + }, } - if (type === WebhookType.CONFIRMATION_REQUEST) { - const { safeTxHash } = data + // Can be safely casted as `data.type` is a mapped type of `NotificationsMap` + const notification = await Notifications[data.type](data as any) + if (notification) { return { - title: `Confirmation request`, - body: `Safe ${shortSafeAddress} on ${chainName} has a new confirmation request for transaction ${shortenAddress( - safeTxHash, - )}.`, - link: getSafeTxHashLink(safeTxHash), + ...notification, + link: getLink(data, chain?.shortName), } } - - if (type === WebhookType.SAFE_CREATED) { - // Notifications are subscribed to per Safe so we would only show this notification - // if the user was subscribed to a pre-determined address - } } export const parseFirebaseNotification = async ( From 65083392c74a7cd4b9667995550ba8a6446b42ec Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 14:58:15 +0200 Subject: [PATCH 30/62] fix: register for confirmation requests by default --- .../GlobalPushNotifications.tsx | 27 ++++++- .../PushNotificationsBanner/index.tsx | 43 +++++++--- .../hooks/useNotificationPreferences.ts | 15 ++-- .../hooks/useNotificationRegistrations.ts | 80 ++++++++----------- .../settings/PushNotifications/index.tsx | 56 ++++++++----- .../settings/PushNotifications/logic.test.ts | 38 --------- .../settings/PushNotifications/logic.ts | 10 +-- 7 files changed, 134 insertions(+), 135 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 8bdec9773d..beb745afe5 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -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' @@ -85,6 +86,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { // Safes selected in the UI const [selectedSafes, setSelectedSafes] = useState({}) + const selectedChains = Object.keys(selectedSafes) // Current Safes registered for notifications in indexedDB const currentNotifiedSafes = useMemo(() => { @@ -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(() => { @@ -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 }) @@ -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)) @@ -232,9 +239,21 @@ export const GlobalPushNotifications = (): ReactElement | null => { My Safes ({totalNotifiableSafes}) - +
+ {totalSignaturesRequired > 0 && ( + + You will have to verify with your signature {totalSignaturesRequired} times + + )} + + + {(isOk) => ( + + )} + +
diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index 87a4f1622f..b12c94adf8 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -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(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(() => { @@ -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() } @@ -82,13 +90,24 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } Enable push notifications - 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.
{totalAddedSafes > 0 && ( - + + {(isOk) => ( + + )} + )} {safe && ( void - _createPreferences: (safesToRegister: NotifiableSafes, withConfirmationRequests?: boolean) => void + _createPreferences: (safesToRegister: NotifiableSafes) => void _deletePreferences: (safesToUnregister: NotifiableSafes) => void _clearPreferences: () => void } => { @@ -124,10 +124,7 @@ export const useNotificationPreferences = (): { }, [hydratePreferences]) // Add store entry with default preferences for specified Safe(s) - const createPreferences = ( - safesToRegister: { [chain: string]: Array }, - withConfirmationRequests?: boolean, - ) => { + const createPreferences = (safesToRegister: { [chain: string]: Array }) => { if (!preferencesStore) { return } @@ -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] diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index 0edb2664ce..ef41029fcf 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -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, 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 unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise @@ -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 ${ @@ -65,30 +73,12 @@ export const useNotificationRegistrations = (): { groupKey: 'notifications', }), ) - } - } - - const unregisterNotifications = async (unregistrationFn: Promise, 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) }) @@ -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) }) diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index 2dd01e8ef1..9a795ca280 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -26,6 +26,7 @@ import { showNotification } from '@/store/notificationsSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { AppRoutes } from '@/config/routes' +import CheckWallet from '@/components/common/CheckWallet' import css from './styles.module.css' @@ -34,8 +35,9 @@ export const PushNotifications = (): ReactElement => { const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() - const { updatePreferences, getPreferences } = useNotificationPreferences() - const { unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() + const { updatePreferences, getPreferences, getAllPreferences } = useNotificationPreferences() + const { unregisterSafeNotifications, unregisterChainNotifications, registerNotifications } = + useNotificationRegistrations() const preferences = getPreferences(safe.chainId, safe.address.value) @@ -46,14 +48,26 @@ export const PushNotifications = (): ReactElement => { const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') const shouldShowMacHelper = isMac || IS_DEV - const handleOnChange = () => { - if (preferences) { - unregisterSafeNotifications(safe.chainId, safe.address.value) - trackEvent(PUSH_NOTIFICATION_EVENTS.DISABLE_SAFE) - } else { - registerNotifications({ [safe.chainId]: [safe.address.value] }) + const handleOnChange = async () => { + if (!preferences) { + await registerNotifications({ [safe.chainId]: [safe.address.value] }) trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_SAFE) + return + } + + const allPreferences = getAllPreferences() + const totalRegisteredSafesOnChain = allPreferences + ? Object.values(allPreferences).filter(({ chainId }) => chainId === safe.chainId).length + : 0 + const shouldUnregisterDevice = totalRegisteredSafesOnChain === 1 + + if (shouldUnregisterDevice) { + await unregisterChainNotifications(safe.chainId) + } else { + await unregisterSafeNotifications(safe.chainId, safe.address.value) } + + trackEvent(PUSH_NOTIFICATION_EVENTS.DISABLE_SAFE) } return ( @@ -69,8 +83,8 @@ export const PushNotifications = (): ReactElement => { - Enable push notifications for {safeLoaded ? 'this Safe Account' : 'your Safe Accounts'} in your browser. - You will need to enable them again if you clear your browser cache. + Enable push notifications for {safeLoaded ? 'this Safe Account' : 'your Safe Accounts'} in your browser + with your signature. You will need to enable them again if you clear your browser cache. {shouldShowMacHelper && ( @@ -97,10 +111,15 @@ export const PushNotifications = (): ReactElement => { showName={true} hasExplorer /> - } - label={preferences ? 'On' : 'Off'} - /> + + {(isOk) => ( + } + label={preferences ? 'On' : 'Off'} + disabled={!isOk} + /> + )} +
@@ -198,12 +217,9 @@ export const PushNotifications = (): ReactElement => { { - registerNotifications( - { - [safe.chainId]: [safe.address.value], - }, - true, // Add signature - ) + registerNotifications({ + [safe.chainId]: [safe.address.value], + }) .then(() => { setPreferences({ ...preferences, diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts index c15176a453..83d91164fc 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/logic.test.ts @@ -89,44 +89,6 @@ describe('Notifications', () => { }) describe('getRegisterDevicePayload', () => { - it('should return the payload without signature', async () => { - const token = crypto.randomUUID() - - jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) - - const uuid = crypto.randomUUID() - - const payload = await logic.getRegisterDevicePayload({ - safesToRegister: { - ['1']: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], - ['2']: [hexZeroPad('0x1', 20)], - }, - uuid, - }) - - expect(payload).toStrictEqual({ - uuid, - cloudMessagingToken: token, - buildNumber: '0', - bundle: 'https://app.safe.global', - deviceType: DeviceType.WEB, - version: packageJson.version, - timestamp: expect.any(String), - safeRegistrations: [ - { - chainId: '1', - safes: [hexZeroPad('0x1', 20), hexZeroPad('0x2', 20)], - signatures: [], - }, - { - chainId: '2', - safes: [hexZeroPad('0x1', 20)], - signatures: [], - }, - ], - }) - }) - it('should return the payload with signature', async () => { const token = crypto.randomUUID() jest.spyOn(firebase, 'getToken').mockImplementation(() => Promise.resolve(token)) diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 22a9a8089d..4d50bac43f 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -68,7 +68,7 @@ export const getRegisterDevicePayload = async ({ }: { safesToRegister: NotifiableSafes uuid: string - web3?: Web3Provider + web3: Web3Provider }): Promise => { const BUILD_NUMBER = '0' // Required value, but does not exist on web const BUNDLE = 'safe' @@ -95,15 +95,13 @@ export const getRegisterDevicePayload = async ({ const safeRegistrations = await Promise.all( Object.entries(safesToRegister).map(async ([chainId, safeAddresses]) => { - // Signature is only required for CONFIRMATION_REQUESTS - const signature = web3 - ? await getSafeRegistrationSignature({ safeAddresses, web3, uuid, timestamp, token }) - : undefined + // We require a signature for confirmation request notifications + const signature = await getSafeRegistrationSignature({ safeAddresses, web3, uuid, timestamp, token }) return { chainId, safes: safeAddresses, - signatures: signature ? [signature] : [], + signatures: [signature], } }), ) From b87a259c0fd2d292c2f1e2b70e77044e9a64bce0 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 15:32:43 +0200 Subject: [PATCH 31/62] fix: adjust text + fix tests --- .../GlobalPushNotifications.tsx | 2 +- .../useNotificationRegistrations.test.ts | 87 +++---------------- 2 files changed, 12 insertions(+), 77 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index beb745afe5..58672806c9 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -242,7 +242,7 @@ export const GlobalPushNotifications = (): ReactElement | null => {
{totalSignaturesRequired > 0 && ( - You will have to verify with your signature {totalSignaturesRequired} times + We'll ask you to verify with your signature {totalSignaturesRequired} times )} diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index 2cfc4520d0..130b2d2bab 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -1,9 +1,11 @@ import { hexZeroPad } from 'ethers/lib/utils' import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' +import { Web3Provider } from '@ethersproject/providers' import * as sdk from '@safe-global/safe-gateway-typescript-sdk' import { renderHook } from '@/tests/test-utils' import { useNotificationRegistrations } from '../useNotificationRegistrations' +import * as web3 from '@/hooks/wallets/web3' import * as logic from '../../logic' import * as preferences from '../useNotificationPreferences' import * as notificationsSlice from '@/store/notificationsSlice' @@ -24,11 +26,15 @@ describe('useNotificationRegistrations', () => { }) describe('registerNotifications', () => { + beforeEach(() => { + const mockProvider = new Web3Provider(jest.fn()) + jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) + }) + const registerDeviceSpy = jest.spyOn(sdk, 'registerDevice') const getExampleRegisterDevicePayload = ( safesToRegister: logic.NotifiableSafes, - withSignatures = false, ): logic.NotificationRegistration => { const safeRegistrations = Object.entries(safesToRegister).reduce< logic.NotificationRegistration['safeRegistrations'] @@ -36,7 +42,7 @@ describe('useNotificationRegistrations', () => { const safeRegistration: logic.NotificationRegistration['safeRegistrations'][number] = { chainId, safes: safeAddresses, - signatures: withSignatures ? [hexZeroPad('0x69420', 65)] : [], + signatures: [hexZeroPad('0x69420', 65)], } acc.push(safeRegistration) @@ -135,9 +141,10 @@ describe('useNotificationRegistrations', () => { expect(createPreferencesMock).not.toHaveBeenCalledWith() }) - it('creates preferences if registration succeeds without signature for a single Safe Account', async () => { + it('creates preferences/notifies if registration succeeded', async () => { const safesToRegister: logic.NotifiableSafes = { '1': [hexZeroPad('0x1', 20)], + '2': [hexZeroPad('0x2', 20)], } const payload = getExampleRegisterDevicePayload(safesToRegister) @@ -160,90 +167,18 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.registerNotifications(safesToRegister) + await result.current.registerNotifications(safesToRegister, true) expect(registerDeviceSpy).toHaveBeenCalledWith(payload) expect(createPreferencesMock).toHaveBeenCalled() expect(showNotificationSpy).toHaveBeenCalledWith({ - message: 'You will now receive notifications for this Safe Account in your browser.', - variant: 'success', groupKey: 'notifications', - }) - }) - - it('creates preferences if registration succeeds without signature for multiple Safe Accounts', async () => { - const safesToRegister: logic.NotifiableSafes = { - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - } - - const payload = getExampleRegisterDevicePayload(safesToRegister) - - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) - - const createPreferencesMock = jest.fn() - - ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( - () => - ({ - uuid: self.crypto.randomUUID(), - _createPreferences: createPreferencesMock, - } as unknown as ReturnType), - ) - - const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') - - const { result } = renderHook(() => useNotificationRegistrations()) - - await result.current.registerNotifications(safesToRegister) - - expect(registerDeviceSpy).toHaveBeenCalledWith(payload) - - expect(createPreferencesMock).toHaveBeenCalled() - - expect(showNotificationSpy).toHaveBeenCalledWith({ message: 'You will now receive notifications for these Safe Accounts in your browser.', variant: 'success', - groupKey: 'notifications', }) }) - - it('creates preferences/does not notify if registration succeeded with signature', async () => { - const safesToRegister: logic.NotifiableSafes = { - '1': [hexZeroPad('0x1', 20)], - '2': [hexZeroPad('0x2', 20)], - } - - const payload = getExampleRegisterDevicePayload(safesToRegister) - - jest.spyOn(logic, 'getRegisterDevicePayload').mockImplementation(() => Promise.resolve(payload)) - - registerDeviceSpy.mockImplementation(() => Promise.resolve()) - - const createPreferencesMock = jest.fn() - - ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( - () => - ({ - uuid: self.crypto.randomUUID(), - _createPreferences: createPreferencesMock, - } as unknown as ReturnType), - ) - - const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') - - const { result } = renderHook(() => useNotificationRegistrations()) - - await result.current.registerNotifications(safesToRegister, true) - - expect(registerDeviceSpy).toHaveBeenCalledWith(payload) - - expect(createPreferencesMock).toHaveBeenCalled() - - expect(showNotificationSpy).not.toHaveBeenCalled() - }) }) describe('unregisterSafeNotifications', () => { From 9e914236647b87ba489f41918c4ddd04e82754cb Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 16:55:04 +0200 Subject: [PATCH 32/62] fix: reduce service worker bundle size --- public/firebase-messaging-sw.ts | 3 +- .../GlobalPushNotifications.tsx | 2 +- .../useNotificationPreferences.test.ts | 14 +-- .../hooks/useNotificationPreferences.ts | 54 ++++++----- .../settings/PushNotifications/logic.test.ts | 2 +- .../settings/PushNotifications/logic.ts | 2 +- src/config/constants.ts | 46 +-------- src/hooks/useFirebaseNotifications.ts | 4 +- .../firebase/{ => __tests__}/index.test.ts | 6 +- src/services/firebase/constants.ts | 49 ++++++++++ src/services/firebase/firebase.ts | 24 +++++ .../firebase/{index.ts => notifications.ts} | 96 ++++++++----------- .../firebase/preferences.ts} | 14 +-- src/services/firebase/webhooks.ts | 2 + src/utils/formatters.ts | 8 +- 15 files changed, 177 insertions(+), 149 deletions(-) rename src/services/firebase/{ => __tests__}/index.test.ts (99%) create mode 100644 src/services/firebase/constants.ts create mode 100644 src/services/firebase/firebase.ts rename src/services/firebase/{index.ts => notifications.ts} (68%) rename src/{components/settings/PushNotifications/hooks/notifications-idb.ts => services/firebase/preferences.ts} (53%) diff --git a/public/firebase-messaging-sw.ts b/public/firebase-messaging-sw.ts index 52f7e5e359..1270562cbe 100644 --- a/public/firebase-messaging-sw.ts +++ b/public/firebase-messaging-sw.ts @@ -3,7 +3,8 @@ import { onBackgroundMessage } from 'firebase/messaging/sw' import { getMessaging } from 'firebase/messaging/sw' -import { initializeFirebase, parseFirebaseNotification, shouldShowNotification } from '@/services/firebase' +import { initializeFirebase } from '@/services/firebase/firebase' +import { parseFirebaseNotification, shouldShowNotification } from '@/services/firebase/notifications' const ICON_PATH = '/images/safe-logo-green.png' diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 58672806c9..5d36d18963 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -26,7 +26,7 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif import { requestNotificationPermission } from './logic' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' -import type { NotificationPreferences } from './hooks/notifications-idb' +import type { NotificationPreferences } from '@/services/firebase/preferences' import CheckWallet from '@/components/common/CheckWallet' import css from './styles.module.css' diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index 8eb16fc9cb..51de7ad865 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -3,7 +3,7 @@ import { set, setMany } from 'idb-keyval' import { renderHook, waitFor } from '@/tests/test-utils' import { hexZeroPad } from 'ethers/lib/utils' -import { createUuidStore, createPreferencesStore } from '../notifications-idb' +import { createNotificationUuidIndexedDb, createNotificationPrefsIndexedDb } from '@/services/firebase/preferences' import { useNotificationPreferences, _DEFAULT_NOTIFICATION_PREFERENCES, @@ -40,7 +40,7 @@ describe('useNotificationPreferences', () => { it('return uuid if it exists', async () => { const uuid = 'test-uuid' - await set('uuid', uuid, createUuidStore()) + await set('uuid', uuid, createNotificationUuidIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -67,7 +67,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createPreferencesStore()) + await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -88,7 +88,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createPreferencesStore()) + await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -146,7 +146,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createPreferencesStore()) + await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -194,7 +194,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createPreferencesStore()) + await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -238,7 +238,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createPreferencesStore()) + await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index e774aa0b39..115be073bb 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -10,11 +10,15 @@ import { useCallback, useEffect, useMemo } from 'react' import { WebhookType } from '@/services/firebase/webhooks' import ExternalStore from '@/services/ExternalStore' -import { createPreferencesStore, createUuidStore, getSafeNotificationKey } from './notifications-idb' -import type { NotificationPreferences, SafeNotificationKey } from './notifications-idb' +import { + createNotificationPrefsIndexedDb, + createNotificationUuidIndexedDb, + getSafeNotificationPrefsKey, +} from '@/services/firebase/preferences' +import type { NotificationPreferences, SafeNotificationPrefsKey } from '@/services/firebase/preferences' import type { NotifiableSafes } from '../logic' -export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationKey]['preferences'] = { +export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationPrefsKey]['preferences'] = { [WebhookType.NEW_CONFIRMATION]: true, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, @@ -42,7 +46,7 @@ export const useNotificationPreferences = (): { updatePreferences: ( chainId: string, safeAddress: string, - preferences: NotificationPreferences[SafeNotificationKey]['preferences'], + preferences: NotificationPreferences[SafeNotificationPrefsKey]['preferences'], ) => void _createPreferences: (safesToRegister: NotifiableSafes) => void _deletePreferences: (safesToUnregister: NotifiableSafes) => void @@ -54,7 +58,7 @@ export const useNotificationPreferences = (): { // Getters const getPreferences = (chainId: string, safeAddress: string) => { - const key = getSafeNotificationKey(chainId, safeAddress) + const key = getSafeNotificationPrefsKey(chainId, safeAddress) return preferences?.[key]?.preferences } @@ -65,13 +69,13 @@ export const useNotificationPreferences = (): { // idb-keyval stores const uuidStore = useMemo(() => { if (typeof indexedDB !== 'undefined') { - return createUuidStore() + return createNotificationUuidIndexedDb() } }, []) const preferencesStore = useMemo(() => { if (typeof indexedDB !== 'undefined') { - return createPreferencesStore() + return createNotificationPrefsIndexedDb() } }, []) @@ -111,7 +115,9 @@ export const useNotificationPreferences = (): { return } - getEntriesFromIndexedDb(preferencesStore) + getEntriesFromIndexedDb( + preferencesStore, + ) .then((preferencesEntries) => { setPreferences(Object.fromEntries(preferencesEntries)) }) @@ -130,17 +136,19 @@ export const useNotificationPreferences = (): { } const defaultPreferencesEntries = Object.entries(safesToRegister).flatMap(([chainId, safeAddresses]) => { - return safeAddresses.map((safeAddress): [SafeNotificationKey, NotificationPreferences[SafeNotificationKey]] => { - const key = getSafeNotificationKey(chainId, safeAddress) - - const defaultPreferences: NotificationPreferences[SafeNotificationKey] = { - chainId, - safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, - } - - return [key, defaultPreferences] - }) + return safeAddresses.map( + (safeAddress): [SafeNotificationPrefsKey, NotificationPreferences[SafeNotificationPrefsKey]] => { + const key = getSafeNotificationPrefsKey(chainId, safeAddress) + + const defaultPreferences: NotificationPreferences[SafeNotificationPrefsKey] = { + chainId, + safeAddress, + preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + } + + return [key, defaultPreferences] + }, + ) }) setManyIndexedDb(defaultPreferencesEntries, preferencesStore) @@ -152,15 +160,15 @@ export const useNotificationPreferences = (): { const updatePreferences = ( chainId: string, safeAddress: string, - preferences: NotificationPreferences[SafeNotificationKey]['preferences'], + preferences: NotificationPreferences[SafeNotificationPrefsKey]['preferences'], ) => { if (!preferencesStore) { return } - const key = getSafeNotificationKey(chainId, safeAddress) + const key = getSafeNotificationPrefsKey(chainId, safeAddress) - const newPreferences: NotificationPreferences[SafeNotificationKey] = { + const newPreferences: NotificationPreferences[SafeNotificationPrefsKey] = { safeAddress, chainId, preferences, @@ -178,7 +186,7 @@ export const useNotificationPreferences = (): { } const keysToDelete = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { - return safeAddresses.map((safeAddress) => getSafeNotificationKey(chainId, safeAddress)) + return safeAddresses.map((safeAddress) => getSafeNotificationPrefsKey(chainId, safeAddress)) }) deleteManyFromIndexedDb(keysToDelete, preferencesStore) diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts index 83d91164fc..f6f55d85ba 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/logic.test.ts @@ -118,7 +118,7 @@ describe('Notifications', () => { uuid, cloudMessagingToken: token, buildNumber: '0', - bundle: 'https://app.safe.global', + bundle: 'safe', deviceType: DeviceType.WEB, version: packageJson.version, timestamp: expect.any(String), diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 4d50bac43f..2c22acce7d 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -4,7 +4,7 @@ import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' -import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/config/constants' +import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/services/firebase/constants' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' diff --git a/src/config/constants.ts b/src/config/constants.ts index afbe284f5c..36bb02c093 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,12 +1,12 @@ import chains from './chains' -import type { FirebaseOptions } from 'firebase/app' +import { _GATEWAY_URL_PRODUCTION, _GATEWAY_URL_STAGING, _IS_PRODUCTION } from '@/services/firebase/constants' -export const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION +// Imported from firebase service worker constants as we want to keep its budle size small +export const IS_PRODUCTION = _IS_PRODUCTION export const IS_DEV = process.env.NODE_ENV === 'development' -export const GATEWAY_URL_PRODUCTION = - process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' -export const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' +export const GATEWAY_URL_PRODUCTION = _GATEWAY_URL_PRODUCTION +export const GATEWAY_URL_STAGING = _GATEWAY_URL_STAGING // Magic numbers export const POLLING_INTERVAL = 15_000 @@ -47,42 +47,6 @@ export const GOOGLE_TAG_MANAGER_AUTH_LIVE = process.env.NEXT_PUBLIC_GOOGLE_TAG_M export const GOOGLE_TAG_MANAGER_AUTH_LATEST = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH || '' export const GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH || '' -// Firebase Cloud Messaging -const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' -const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' -const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' -const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' -const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' -const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' -const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' -const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' -const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' - -const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' -const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' -const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' -const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' -const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' -const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' -const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' -const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' -const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' - -export const FIREBASE_OPTIONS: FirebaseOptions = { - apiKey: IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, - authDomain: IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, - databaseURL: IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, - projectId: IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, - storageBucket: IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, - messagingSenderId: IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, - appId: IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, - measurementId: IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, -} as const - -export const FIREBASE_VAPID_KEY = IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING - -export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' - // Tenderly - API docs: https://www.notion.so/Simulate-API-Documentation-6f7009fe6d1a48c999ffeb7941efc104 export const TENDERLY_SIMULATE_ENDPOINT_URL = process.env.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL || '' export const TENDERLY_PROJECT_NAME = process.env.NEXT_PUBLIC_TENDERLY_PROJECT_NAME || '' diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index b6c9bee28d..e1d2ebc5d9 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' -import { initializeFirebase } from '@/services/firebase' -import { FIREBASE_MESSAGING_SW_PATH } from '@/config/constants' +import { initializeFirebase } from '@/services/firebase/firebase' +import { FIREBASE_MESSAGING_SW_PATH } from '@/services/firebase/constants' export const useFirebaseNotifications = (): void => { // Register servicer worker diff --git a/src/services/firebase/index.test.ts b/src/services/firebase/__tests__/index.test.ts similarity index 99% rename from src/services/firebase/index.test.ts rename to src/services/firebase/__tests__/index.test.ts index 034f842a0f..5f5f0662ec 100644 --- a/src/services/firebase/index.test.ts +++ b/src/services/firebase/__tests__/index.test.ts @@ -1,8 +1,8 @@ import { hexZeroPad } from 'ethers/lib/utils' import type { ChainInfo, TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { _parseWebhookNotification } from '.' -import { WebhookType } from './webhooks' +import { _parseWebhookNotification } from '../notifications' +import { WebhookType } from '../webhooks' import type { ConfirmationRequestEvent, ExecutedMultisigTransactionEvent, @@ -14,7 +14,7 @@ import type { OutgoingTokenEvent, PendingMultisigTransactionEvent, SafeCreatedEvent, -} from './webhooks' +} from '../webhooks' const setupFetchStub = (data: any) => (_url: string) => { return Promise.resolve({ diff --git a/src/services/firebase/constants.ts b/src/services/firebase/constants.ts new file mode 100644 index 0000000000..95a0a3d694 --- /dev/null +++ b/src/services/firebase/constants.ts @@ -0,0 +1,49 @@ +// Refrain from importing outside of this folder to keep firebase-sw.js bundle small + +import type { FirebaseOptions } from 'firebase/app' + +// The following are re-exported in our constants file to avoid incorrect contants +export const _IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION + +export const _GATEWAY_URL_PRODUCTION = + process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' +export const _GATEWAY_URL_STAGING = + process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' + +// localStorage cannot be accessed in service workers so we reference the flag +export const _GATEWAY_URL = _IS_PRODUCTION ? _GATEWAY_URL_PRODUCTION : _GATEWAY_URL_STAGING + +export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' + +const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' +const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' +const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' +const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' +const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' +const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' +const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' +const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' +const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' + +const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' +const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' +const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' +const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' +const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' +const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' +const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' +const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' +const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' + +export const FIREBASE_VAPID_KEY = _IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING + +export const FIREBASE_OPTIONS: FirebaseOptions = { + apiKey: _IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, + authDomain: _IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, + databaseURL: _IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, + projectId: _IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, + storageBucket: _IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, + messagingSenderId: _IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, + appId: _IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, + measurementId: _IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, +} as const diff --git a/src/services/firebase/firebase.ts b/src/services/firebase/firebase.ts new file mode 100644 index 0000000000..22849b1a3c --- /dev/null +++ b/src/services/firebase/firebase.ts @@ -0,0 +1,24 @@ +// Refrain from importing outside of this folder to keep firebase-sw.js bundle small + +import { initializeApp } from 'firebase/app' +import type { FirebaseApp } from 'firebase/app' + +import { FIREBASE_OPTIONS } from './constants' + +export const initializeFirebase = () => { + const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) + + if (!hasFirebaseOptions) { + return + } + + let app: FirebaseApp | null = null + + try { + app = initializeApp(FIREBASE_OPTIONS) + } catch (e) { + console.error('[Firebase] Initialization failed', e) + } + + return app +} diff --git a/src/services/firebase/index.ts b/src/services/firebase/notifications.ts similarity index 68% rename from src/services/firebase/index.ts rename to src/services/firebase/notifications.ts index 51304ebc5b..609ffd9835 100644 --- a/src/services/firebase/index.ts +++ b/src/services/firebase/notifications.ts @@ -1,39 +1,24 @@ -import { formatUnits } from 'ethers/lib/utils' -import { get as getFromIndexedDb } from 'idb-keyval' -import { initializeApp } from 'firebase/app' -import type { MessagePayload } from 'firebase/messaging/sw' -import type { ChainInfo, SafeBalanceResponse, ChainListResponse } from '@safe-global/safe-gateway-typescript-sdk' - -import { shortenAddress } from '@/utils/formatters' -import { AppRoutes } from '@/config/routes' -import { isWebhookEvent, WebhookType } from '@/services/firebase/webhooks' -import type { WebhookEvent } from '@/services/firebase/webhooks' -import { FIREBASE_OPTIONS, GATEWAY_URL_PRODUCTION, GATEWAY_URL_STAGING, IS_PRODUCTION } from '@/config/constants' -import { - createPreferencesStore, - getSafeNotificationKey, -} from '@/components/settings/PushNotifications/hooks/notifications-idb' -import type { - NotificationPreferences, - SafeNotificationKey, -} from '@/components/settings/PushNotifications/hooks/notifications-idb' - -export const initializeFirebase = () => { - const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) - - if (!hasFirebaseOptions) { - return - } - - let app: ReturnType | null = null +// Refrain from importing outside of this folder to keep firebase-sw.js bundle small - try { - app = initializeApp(FIREBASE_OPTIONS) - } catch (e) { - console.error('[Firebase] Initialization failed', e) +import { get as getFromIndexedDb } from 'idb-keyval' +import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable +import type { ChainInfo, ChainListResponse, SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' +import type { MessagePayload } from 'firebase/messaging' + +import { AppRoutes } from '@/config/routes' // Has no internal imports +import { _GATEWAY_URL } from './constants' +import { isWebhookEvent, WebhookType } from './webhooks' +import { getSafeNotificationPrefsKey, createNotificationUuidIndexedDb } from './preferences' +import type { WebhookEvent } from './webhooks' +import type { NotificationPreferences, SafeNotificationPrefsKey } from './preferences' + +// Export for formatters to keep bundle small +export const _shortenAddress = (address: string, length = 4): string => { + if (!address) { + return '' } - return app + return `${address.slice(0, length + 2)}...${address.slice(-length)}` } export const shouldShowNotification = async (payload: MessagePayload): Promise => { @@ -43,10 +28,10 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise(key, store).catch( + const preferencesStore = await getFromIndexedDb(key, store).catch( () => null, ) @@ -58,12 +43,11 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise => { - const ENDPOINT = `${BASE_URL}/v1/chains` + const ENDPOINT = `${_GATEWAY_URL}/v1/chains` let chains: ChainListResponse | null = null @@ -84,7 +68,7 @@ const getTokenInfo = async ( tokenValue?: string, ): Promise<{ symbol: string; value: string; name: string }> => { const DEFAULT_CURRENCY = 'USD' - const ENDPOINT = `${BASE_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` + const ENDPOINT = `${_GATEWAY_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` const DEFAULT_INFO = { symbol: 'tokens', @@ -160,24 +144,24 @@ export const _parseWebhookNotification = async ( [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash }) => { return { title: 'Transaction confirmation', - body: `Safe ${shortenAddress(address)} on ${chainName} has a new confirmation from ${shortenAddress( + body: `Safe ${_shortenAddress(address)} on ${chainName} has a new confirmation from ${_shortenAddress( owner, - )} on transaction ${shortenAddress(safeTxHash)}.`, + )} on transaction ${_shortenAddress(safeTxHash)}.`, } }, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash }) => { const didFail = failed === 'true' return { title: `Transaction ${didFail ? 'failed' : 'executed'}`, - body: `Safe ${shortenAddress(address)} on ${chainName} ${ + body: `Safe ${_shortenAddress(address)} on ${chainName} ${ didFail ? 'failed to execute' : 'executed' - } transaction ${shortenAddress(txHash)}.`, + } transaction ${_shortenAddress(txHash)}.`, } }, [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash }) => { return { title: 'Pending transaction', - body: `Safe ${shortenAddress(address)} on ${chainName} has a pending transaction ${shortenAddress( + body: `Safe ${_shortenAddress(address)} on ${chainName} has a pending transaction ${_shortenAddress( safeTxHash, )}.`, } @@ -185,53 +169,53 @@ export const _parseWebhookNotification = async ( [WebhookType.INCOMING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} received`, - body: `Safe ${shortenAddress(address)} on ${chainName} received ${formatUnits( + body: `Safe ${_shortenAddress(address)} on ${chainName} received ${formatUnits( value, chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, + ).toString()} ${currencySymbol} in transaction ${_shortenAddress(txHash)}.`, } }, [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} sent`, - body: `Safe ${shortenAddress(address)} on ${chainName} sent ${formatUnits( + body: `Safe ${_shortenAddress(address)} on ${chainName} sent ${formatUnits( value, chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, + ).toString()} ${currencySymbol} in transaction ${_shortenAddress(txHash)}.`, } }, [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { title: `${token.name} received`, - body: `Safe ${shortenAddress(address)} on ${chainName} received ${token.value} ${ + body: `Safe ${_shortenAddress(address)} on ${chainName} received ${token.value} ${ token.symbol - } in transaction ${shortenAddress(txHash)}.`, + } in transaction ${_shortenAddress(txHash)}.`, } }, [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { title: `${token.name} sent`, - body: `Safe ${shortenAddress(address)} on ${chainName} sent ${token.value} ${ + body: `Safe ${_shortenAddress(address)} on ${chainName} sent ${token.value} ${ token.symbol - } in transaction ${shortenAddress(txHash)}.`, + } in transaction ${_shortenAddress(txHash)}.`, } }, [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash }) => { return { title: 'Module transaction', - body: `Safe ${shortenAddress(address)} on ${chainName} executed a module transaction ${shortenAddress( + body: `Safe ${_shortenAddress(address)} on ${chainName} executed a module transaction ${_shortenAddress( txHash, - )} from module ${shortenAddress(module)}.`, + )} from module ${_shortenAddress(module)}.`, } }, [WebhookType.CONFIRMATION_REQUEST]: ({ address, safeTxHash }) => { return { title: 'Confirmation request', - body: `Safe ${shortenAddress( + body: `Safe ${_shortenAddress( address, - )} on ${chainName} has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.`, + )} on ${chainName} has a new confirmation request for transaction ${_shortenAddress(safeTxHash)}.`, } }, [WebhookType.SAFE_CREATED]: () => { diff --git a/src/components/settings/PushNotifications/hooks/notifications-idb.ts b/src/services/firebase/preferences.ts similarity index 53% rename from src/components/settings/PushNotifications/hooks/notifications-idb.ts rename to src/services/firebase/preferences.ts index 6cd21f44e6..5ad29e67d4 100644 --- a/src/components/settings/PushNotifications/hooks/notifications-idb.ts +++ b/src/services/firebase/preferences.ts @@ -1,29 +1,31 @@ +// Refrain from importing outside of this folder to keep firebase-sw.js bundle small + import { createStore } from 'idb-keyval' -import type { WebhookType } from '@/services/firebase/webhooks' +import type { WebhookType } from './webhooks' -export const createUuidStore = () => { +export const createNotificationUuidIndexedDb = () => { const DB_NAME = 'notifications-uuid-database' const STORE_NAME = 'notifications-uuid-store' return createStore(DB_NAME, STORE_NAME) } -export type SafeNotificationKey = `${string}:${string}` +export type SafeNotificationPrefsKey = `${string}:${string}` export type NotificationPreferences = { - [safeKey: SafeNotificationKey]: { + [safeKey: SafeNotificationPrefsKey]: { chainId: string safeAddress: string preferences: { [key in WebhookType]: boolean } } } -export const getSafeNotificationKey = (chainId: string, safeAddress: string): SafeNotificationKey => { +export const getSafeNotificationPrefsKey = (chainId: string, safeAddress: string): SafeNotificationPrefsKey => { return `${chainId}:${safeAddress}` } -export const createPreferencesStore = () => { +export const createNotificationPrefsIndexedDb = () => { const DB_NAME = 'notifications-preferences-database' const STORE_NAME = 'notifications-preferences-store' diff --git a/src/services/firebase/webhooks.ts b/src/services/firebase/webhooks.ts index 693e525af6..0fef2ddbe2 100644 --- a/src/services/firebase/webhooks.ts +++ b/src/services/firebase/webhooks.ts @@ -1,3 +1,5 @@ +// Refrain from importing outside of this folder to keep firebase-sw.js bundle small + import type { MessagePayload } from 'firebase/messaging' export const isWebhookEvent = (data: MessagePayload['data']): data is WebhookEvent => { diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index c5770232bd..7d3f619ac7 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -51,13 +51,7 @@ export const safeParseUnits = (value: string, decimals: number | string = GWEI): } } -export const shortenAddress = (address: string, length = 4): string => { - if (!address) { - return '' - } - - return `${address.slice(0, length + 2)}...${address.slice(-length)}` -} +export { _shortenAddress as shortenAddress } from '@/services/firebase/notifications' export const shortenText = (text: string, length = 10, separator = '...'): string => { return `${text.slice(0, length)}${separator}` From ffcfd00ce7ddc07bbe03aa8b4c661191cacfc06e Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 31 Aug 2023 17:24:28 +0200 Subject: [PATCH 33/62] fix: don't re-export --- .../settings/PushNotifications/logic.ts | 2 +- src/config/constants.ts | 9 ++-- src/hooks/useFirebaseNotifications.ts | 2 +- src/services/firebase/constants.ts | 49 ------------------- src/services/firebase/firebase.ts | 39 ++++++++++++++- src/services/firebase/notifications.ts | 48 +++++++++--------- src/utils/formatters.ts | 8 ++- 7 files changed, 75 insertions(+), 82 deletions(-) delete mode 100644 src/services/firebase/constants.ts diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 2c22acce7d..8156b07194 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -4,7 +4,7 @@ import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' -import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/services/firebase/constants' +import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/services/firebase/firebase' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' diff --git a/src/config/constants.ts b/src/config/constants.ts index 36bb02c093..a148aa3caa 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,12 +1,11 @@ import chains from './chains' -import { _GATEWAY_URL_PRODUCTION, _GATEWAY_URL_STAGING, _IS_PRODUCTION } from '@/services/firebase/constants' -// Imported from firebase service worker constants as we want to keep its budle size small -export const IS_PRODUCTION = _IS_PRODUCTION +export const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION export const IS_DEV = process.env.NODE_ENV === 'development' -export const GATEWAY_URL_PRODUCTION = _GATEWAY_URL_PRODUCTION -export const GATEWAY_URL_STAGING = _GATEWAY_URL_STAGING +export const GATEWAY_URL_PRODUCTION = + process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' +export const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' // Magic numbers export const POLLING_INTERVAL = 15_000 diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index e1d2ebc5d9..6f835ed692 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { initializeFirebase } from '@/services/firebase/firebase' -import { FIREBASE_MESSAGING_SW_PATH } from '@/services/firebase/constants' +import { FIREBASE_MESSAGING_SW_PATH } from '@/services/firebase/firebase' export const useFirebaseNotifications = (): void => { // Register servicer worker diff --git a/src/services/firebase/constants.ts b/src/services/firebase/constants.ts deleted file mode 100644 index 95a0a3d694..0000000000 --- a/src/services/firebase/constants.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Refrain from importing outside of this folder to keep firebase-sw.js bundle small - -import type { FirebaseOptions } from 'firebase/app' - -// The following are re-exported in our constants file to avoid incorrect contants -export const _IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION - -export const _GATEWAY_URL_PRODUCTION = - process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' -export const _GATEWAY_URL_STAGING = - process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' - -// localStorage cannot be accessed in service workers so we reference the flag -export const _GATEWAY_URL = _IS_PRODUCTION ? _GATEWAY_URL_PRODUCTION : _GATEWAY_URL_STAGING - -export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' - -const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' -const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' -const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' -const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' -const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' -const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' -const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' -const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' -const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' - -const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' -const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' -const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' -const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' -const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' -const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' -const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' -const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' -const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' - -export const FIREBASE_VAPID_KEY = _IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING - -export const FIREBASE_OPTIONS: FirebaseOptions = { - apiKey: _IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, - authDomain: _IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, - databaseURL: _IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, - projectId: _IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, - storageBucket: _IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, - messagingSenderId: _IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, - appId: _IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, - measurementId: _IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, -} as const diff --git a/src/services/firebase/firebase.ts b/src/services/firebase/firebase.ts index 22849b1a3c..b77557811d 100644 --- a/src/services/firebase/firebase.ts +++ b/src/services/firebase/firebase.ts @@ -1,9 +1,44 @@ // Refrain from importing outside of this folder to keep firebase-sw.js bundle small import { initializeApp } from 'firebase/app' -import type { FirebaseApp } from 'firebase/app' +import type { FirebaseApp, FirebaseOptions } from 'firebase/app' -import { FIREBASE_OPTIONS } from './constants' +const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION + +export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' + +const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' +const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' +const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' +const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' +const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' +const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' +const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' +const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' +const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' + +const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' +const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' +const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' +const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' +const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' +const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' +const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' +const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' +const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' + +export const FIREBASE_VAPID_KEY = IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING + +export const FIREBASE_OPTIONS: FirebaseOptions = { + apiKey: IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, + authDomain: IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, + databaseURL: IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, + projectId: IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, + storageBucket: IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, + messagingSenderId: IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, + appId: IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, + measurementId: IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, +} as const export const initializeFirebase = () => { const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) diff --git a/src/services/firebase/notifications.ts b/src/services/firebase/notifications.ts index 609ffd9835..d46fbbcdba 100644 --- a/src/services/firebase/notifications.ts +++ b/src/services/firebase/notifications.ts @@ -6,14 +6,12 @@ import type { ChainInfo, ChainListResponse, SafeBalanceResponse } from '@safe-gl import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports -import { _GATEWAY_URL } from './constants' import { isWebhookEvent, WebhookType } from './webhooks' import { getSafeNotificationPrefsKey, createNotificationUuidIndexedDb } from './preferences' import type { WebhookEvent } from './webhooks' import type { NotificationPreferences, SafeNotificationPrefsKey } from './preferences' -// Export for formatters to keep bundle small -export const _shortenAddress = (address: string, length = 4): string => { +const shortenAddress = (address: string, length = 4): string => { if (!address) { return '' } @@ -42,12 +40,16 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise => { - const ENDPOINT = `${_GATEWAY_URL}/v1/chains` + const ENDPOINT = `${GATEWAY_URL}/v1/chains` let chains: ChainListResponse | null = null @@ -68,7 +70,7 @@ const getTokenInfo = async ( tokenValue?: string, ): Promise<{ symbol: string; value: string; name: string }> => { const DEFAULT_CURRENCY = 'USD' - const ENDPOINT = `${_GATEWAY_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` + const ENDPOINT = `${GATEWAY_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` const DEFAULT_INFO = { symbol: 'tokens', @@ -144,24 +146,24 @@ export const _parseWebhookNotification = async ( [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash }) => { return { title: 'Transaction confirmation', - body: `Safe ${_shortenAddress(address)} on ${chainName} has a new confirmation from ${_shortenAddress( + body: `Safe ${shortenAddress(address)} on ${chainName} has a new confirmation from ${shortenAddress( owner, - )} on transaction ${_shortenAddress(safeTxHash)}.`, + )} on transaction ${shortenAddress(safeTxHash)}.`, } }, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash }) => { const didFail = failed === 'true' return { title: `Transaction ${didFail ? 'failed' : 'executed'}`, - body: `Safe ${_shortenAddress(address)} on ${chainName} ${ + body: `Safe ${shortenAddress(address)} on ${chainName} ${ didFail ? 'failed to execute' : 'executed' - } transaction ${_shortenAddress(txHash)}.`, + } transaction ${shortenAddress(txHash)}.`, } }, [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash }) => { return { title: 'Pending transaction', - body: `Safe ${_shortenAddress(address)} on ${chainName} has a pending transaction ${_shortenAddress( + body: `Safe ${shortenAddress(address)} on ${chainName} has a pending transaction ${shortenAddress( safeTxHash, )}.`, } @@ -169,53 +171,53 @@ export const _parseWebhookNotification = async ( [WebhookType.INCOMING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} received`, - body: `Safe ${_shortenAddress(address)} on ${chainName} received ${formatUnits( + body: `Safe ${shortenAddress(address)} on ${chainName} received ${formatUnits( value, chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${_shortenAddress(txHash)}.`, + ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, } }, [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value }) => { return { title: `${currencyName} sent`, - body: `Safe ${_shortenAddress(address)} on ${chainName} sent ${formatUnits( + body: `Safe ${shortenAddress(address)} on ${chainName} sent ${formatUnits( value, chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${_shortenAddress(txHash)}.`, + ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, } }, [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { title: `${token.name} received`, - body: `Safe ${_shortenAddress(address)} on ${chainName} received ${token.value} ${ + body: `Safe ${shortenAddress(address)} on ${chainName} received ${token.value} ${ token.symbol - } in transaction ${_shortenAddress(txHash)}.`, + } in transaction ${shortenAddress(txHash)}.`, } }, [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { const token = await getTokenInfo(data.chainId, address, tokenAddress, value) return { title: `${token.name} sent`, - body: `Safe ${_shortenAddress(address)} on ${chainName} sent ${token.value} ${ + body: `Safe ${shortenAddress(address)} on ${chainName} sent ${token.value} ${ token.symbol - } in transaction ${_shortenAddress(txHash)}.`, + } in transaction ${shortenAddress(txHash)}.`, } }, [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash }) => { return { title: 'Module transaction', - body: `Safe ${_shortenAddress(address)} on ${chainName} executed a module transaction ${_shortenAddress( + body: `Safe ${shortenAddress(address)} on ${chainName} executed a module transaction ${shortenAddress( txHash, - )} from module ${_shortenAddress(module)}.`, + )} from module ${shortenAddress(module)}.`, } }, [WebhookType.CONFIRMATION_REQUEST]: ({ address, safeTxHash }) => { return { title: 'Confirmation request', - body: `Safe ${_shortenAddress( + body: `Safe ${shortenAddress( address, - )} on ${chainName} has a new confirmation request for transaction ${_shortenAddress(safeTxHash)}.`, + )} on ${chainName} has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.`, } }, [WebhookType.SAFE_CREATED]: () => { diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 7d3f619ac7..c5770232bd 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -51,7 +51,13 @@ export const safeParseUnits = (value: string, decimals: number | string = GWEI): } } -export { _shortenAddress as shortenAddress } from '@/services/firebase/notifications' +export const shortenAddress = (address: string, length = 4): string => { + if (!address) { + return '' + } + + return `${address.slice(0, length + 2)}...${address.slice(-length)}` +} export const shortenText = (text: string, length = 10, separator = '...'): string => { return `${text.slice(0, length)}${separator}` From 7a1ed3d178211167e6b890788a86a17ff179ee6c Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 09:42:49 +0200 Subject: [PATCH 34/62] wip: reimplement next-pwa --- .gitignore | 4 +- next.config.mjs | 52 +- package.json | 10 +- .../GlobalPushNotifications.tsx | 11 +- .../settings/PushNotifications/logic.ts | 6 +- src/hooks/useFirebaseNotifications.ts | 26 +- src/services/firebase/{firebase.ts => app.ts} | 4 +- src/services/firebase/notifications.ts | 2 +- src/services/firebase/preferences.ts | 8 +- src/services/firebase/webhooks.ts | 2 +- .../worker/index.ts | 21 +- tsconfig.json | 3 +- yarn.lock | 809 +++++++++--------- 13 files changed, 471 insertions(+), 487 deletions(-) rename src/services/firebase/{firebase.ts => app.ts} (95%) rename public/firebase-messaging-sw.ts => src/worker/index.ts (75%) diff --git a/.gitignore b/.gitignore index c15f10734c..97916164f6 100644 --- a/.gitignore +++ b/.gitignore @@ -47,11 +47,9 @@ yalc.lock /cypress/screenshots /cypress/downloads -/public/firebase-messaging-sw.js - -# TODO: Remove after we have all deleted these locally - next-pwa remnants /public/sw.js /public/sw.js.map +/public/worker-*.js /public/workbox-*.js /public/workbox-*.js.map /public/fallback* \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index c0b00c7874..a1badd924e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,20 @@ import path from 'path' -import { InjectManifest } from 'workbox-webpack-plugin' import withBundleAnalyzer from '@next/bundle-analyzer' +import withPWAInit from '@ducanh2912/next-pwa' + +const withPWA = withPWAInit({ + dest: 'public', + // reloadOnOnline: false, + // /* Do not precache anything */ + // publicExcludes: ['**/*'], + // buildExcludes: [/./], + // InjectManifest for Web Push + swSrc: './src/worker/index.ts', + // register: false, + // workboxOptions: { + // mode: 'production', + // }, +}) /** @type {import('next').NextConfig} */ const nextConfig = { @@ -29,7 +43,7 @@ const nextConfig = { transform: 'date-fns/{{member}}', }, }, - webpack(config, context) { + webpack(config) { config.module.rules.push({ test: /\.svg$/i, issuer: { and: [/\.(js|ts|md)x?$/] }, @@ -55,38 +69,6 @@ const nextConfig = { ], }) - // Enable hot reloading/compilation of the Firebase service worker in Typescript - // @see https://github.com/vercel/next.js/issues/33863#issuecomment-1140518693 - if (!context.isServer) { - const swSrc = path.join(context.dir, 'public', '/firebase-messaging-sw.ts') - const swDest = path.join(context.dir, 'public', '/firebase-messaging-sw.js') - - const workboxPlugin = new InjectManifest({ - swSrc, - swDest, - include: ['__nothing__'], - }) - - if (context.dev) { - // Suppress the "InjectManifest has been called multiple times" warning by reaching into - // the private properties of the plugin and making sure it never ends up in the state - // where it makes that warning. - // https://github.com/GoogleChrome/workbox/blob/v6/packages/workbox-webpack-plugin/src/inject-manifest.ts#L260-L282 - // @see https://github.com/GoogleChrome/workbox/issues/1790#issuecomment-1241356293 - Object.defineProperty(workboxPlugin, 'alreadyCalled', { - get() { - return false - }, - set() { - // do nothing; the internals try to set it to true, which then results in a warning - // on the next run of webpack. - }, - }) - } - - config.plugins.push(workboxPlugin) - } - config.resolve.alias = { ...config.resolve.alias, 'bn.js': path.resolve('./node_modules/bn.js/lib/bn.js'), @@ -99,4 +81,4 @@ const nextConfig = { export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', -})(nextConfig) +})(withPWA(nextConfig)) diff --git a/package.json b/package.json index cb1c7c5abc..8fec0317e9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@date-io/date-fns": "^2.15.0", + "@ducanh2912/next-pwa": "^9.5.0", "@emotion/cache": "^11.10.1", "@emotion/react": "^11.10.0", "@emotion/server": "^11.10.0", @@ -70,14 +71,13 @@ "ethereum-blockies-base64": "^1.0.2", "ethers": "5.7.2", "exponential-backoff": "^3.1.0", - "firebase": "^10.1.0", + "firebase": "^10.3.1", "framer-motion": "^10.13.1", "fuse.js": "^6.6.2", "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", "next": "^13.2.0", - "next-pwa": "^5.6.0", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", "react": "18.2.0", @@ -88,7 +88,8 @@ "react-papaparse": "^4.0.2", "react-qr-reader": "2.2.1", "react-redux": "^8.0.5", - "semver": "^7.5.2" + "semver": "^7.5.2", + "webpack": "^5.88.2" }, "devDependencies": { "@next/bundle-analyzer": "^13.1.1", @@ -129,7 +130,6 @@ "ts-prune": "^0.10.3", "typechain": "^8.0.0", "typescript": "4.9.4", - "typescript-plugin-css-modules": "^4.2.2", - "workbox-webpack-plugin": "^7.0.0" + "typescript-plugin-css-modules": "^4.2.2" } } diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 5d36d18963..b99bc888e0 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -23,7 +23,7 @@ import { useNotificationRegistrations } from './hooks/useNotificationRegistratio import { selectAllAddedSafes } from '@/store/addedSafesSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' -import { requestNotificationPermission } from './logic' +// import { requestNotificationPermission } from './logic' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { NotificationPreferences } from '@/services/firebase/preferences' @@ -165,11 +165,12 @@ export const GlobalPushNotifications = (): ReactElement | null => { return } - const isGranted = await requestNotificationPermission() + // TODO: Can we remove this? + // const isGranted = await requestNotificationPermission() - if (!isGranted) { - return - } + // if (!isGranted) { + // return + // } const registrationPromises: Array> = [] diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 8156b07194..79adab3395 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -4,7 +4,7 @@ import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' -import { FIREBASE_MESSAGING_SW_PATH, FIREBASE_VAPID_KEY } from '@/services/firebase/firebase' +import { FIREBASE_VAPID_KEY } from '@/services/firebase/app' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' @@ -73,13 +73,13 @@ export const getRegisterDevicePayload = async ({ const BUILD_NUMBER = '0' // Required value, but does not exist on web const BUNDLE = 'safe' - const swRegistration = await navigator.serviceWorker.getRegistration(FIREBASE_MESSAGING_SW_PATH) + const [serviceWorkerRegistration] = await navigator.serviceWorker.getRegistrations() // Get Firebase token const messaging = getMessaging() const token = await getToken(messaging, { vapidKey: FIREBASE_VAPID_KEY, - serviceWorkerRegistration: swRegistration, + serviceWorkerRegistration, }) // If uuid is not provided a new device will be created. diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts index 6f835ed692..67b2874592 100644 --- a/src/hooks/useFirebaseNotifications.ts +++ b/src/hooks/useFirebaseNotifications.ts @@ -1,29 +1,19 @@ import { useEffect } from 'react' -import { initializeFirebase } from '@/services/firebase/firebase' -import { FIREBASE_MESSAGING_SW_PATH } from '@/services/firebase/firebase' +import { initializeFirebase } from '@/services/firebase/app' export const useFirebaseNotifications = (): void => { + // TODO: Can we remove this? // Register servicer worker useEffect(() => { - if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { - return - } + // if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { + // return + // } const app = initializeFirebase() - if (!app) { - return - } - - const registerFirebaseSw = () => { - navigator.serviceWorker.register(FIREBASE_MESSAGING_SW_PATH).catch(() => null) - } - - window.addEventListener('load', registerFirebaseSw) - - return () => { - window.removeEventListener('load', registerFirebaseSw) - } + // if (app) { + // window.workbox.register() + // } }, []) } diff --git a/src/services/firebase/firebase.ts b/src/services/firebase/app.ts similarity index 95% rename from src/services/firebase/firebase.ts rename to src/services/firebase/app.ts index b77557811d..5918e77863 100644 --- a/src/services/firebase/firebase.ts +++ b/src/services/firebase/app.ts @@ -1,12 +1,10 @@ -// Refrain from importing outside of this folder to keep firebase-sw.js bundle small +// Be careful what you import here as it will increase the service worker bundle size import { initializeApp } from 'firebase/app' import type { FirebaseApp, FirebaseOptions } from 'firebase/app' const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION -export const FIREBASE_MESSAGING_SW_PATH = '/firebase-messaging-sw.js' - const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' diff --git a/src/services/firebase/notifications.ts b/src/services/firebase/notifications.ts index d46fbbcdba..7a48e3bf43 100644 --- a/src/services/firebase/notifications.ts +++ b/src/services/firebase/notifications.ts @@ -1,4 +1,4 @@ -// Refrain from importing outside of this folder to keep firebase-sw.js bundle small +// Be careful what you import here as it will increase the service worker bundle size import { get as getFromIndexedDb } from 'idb-keyval' import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable diff --git a/src/services/firebase/preferences.ts b/src/services/firebase/preferences.ts index 5ad29e67d4..04f175f5c7 100644 --- a/src/services/firebase/preferences.ts +++ b/src/services/firebase/preferences.ts @@ -1,6 +1,6 @@ -// Refrain from importing outside of this folder to keep firebase-sw.js bundle small +// Be careful what you import here as it will increase the service worker bundle size -import { createStore } from 'idb-keyval' +import { createStore as createIndexedDb } from 'idb-keyval' import type { WebhookType } from './webhooks' @@ -8,7 +8,7 @@ export const createNotificationUuidIndexedDb = () => { const DB_NAME = 'notifications-uuid-database' const STORE_NAME = 'notifications-uuid-store' - return createStore(DB_NAME, STORE_NAME) + return createIndexedDb(DB_NAME, STORE_NAME) } export type SafeNotificationPrefsKey = `${string}:${string}` @@ -29,5 +29,5 @@ export const createNotificationPrefsIndexedDb = () => { const DB_NAME = 'notifications-preferences-database' const STORE_NAME = 'notifications-preferences-store' - return createStore(DB_NAME, STORE_NAME) + return createIndexedDb(DB_NAME, STORE_NAME) } diff --git a/src/services/firebase/webhooks.ts b/src/services/firebase/webhooks.ts index 0fef2ddbe2..3f1052e4f6 100644 --- a/src/services/firebase/webhooks.ts +++ b/src/services/firebase/webhooks.ts @@ -1,4 +1,4 @@ -// Refrain from importing outside of this folder to keep firebase-sw.js bundle small +// Be careful what you import here as it will increase the service worker bundle size import type { MessagePayload } from 'firebase/messaging' diff --git a/public/firebase-messaging-sw.ts b/src/worker/index.ts similarity index 75% rename from public/firebase-messaging-sw.ts rename to src/worker/index.ts index 1270562cbe..9384a15bfd 100644 --- a/public/firebase-messaging-sw.ts +++ b/src/worker/index.ts @@ -1,20 +1,25 @@ -/// +// Be careful what you import here as it will increase the service worker bundle size -import { onBackgroundMessage } from 'firebase/messaging/sw' -import { getMessaging } from 'firebase/messaging/sw' +// TypeScript +/// -import { initializeFirebase } from '@/services/firebase/firebase' -import { parseFirebaseNotification, shouldShowNotification } from '@/services/firebase/notifications' +import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' -const ICON_PATH = '/images/safe-logo-green.png' +import { initializeFirebase } from '@/services/firebase/app' +import { shouldShowNotification, parseFirebaseNotification } from '@/services/firebase/notifications' // Default type of `self` is `WorkerGlobalScope & typeof globalThis` // https://github.com/microsoft/TypeScript/issues/14877 -declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown } +// TODO: Fix type +declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown; __WB_DISABLE_DEV_LOGS: boolean } -// Satisfy Workbox +// Satisfy workbox self.__WB_MANIFEST +self.__WB_DISABLE_DEV_LOGS = true + +const ICON_PATH = '/images/safe-logo-green.png' + const app = initializeFirebase() if (app) { diff --git a/tsconfig.json b/tsconfig.json index 045cbd3169..51345ab3e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "@/*": ["./src/*"], "@/public/*": ["./public/*"] }, - "plugins": [{ "name": "typescript-plugin-css-modules" }] + "plugins": [{ "name": "typescript-plugin-css-modules" }], + "types": ["@ducanh2912/next-pwa/workbox"] }, "include": ["next-env.d.ts", "src/definitions.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "src/types/contracts"] diff --git a/yarn.lock b/yarn.lock index 95f2a466ad..75f3adec15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1995,6 +1995,20 @@ dependencies: "@date-io/core" "^2.16.0" +"@ducanh2912/next-pwa@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@ducanh2912/next-pwa/-/next-pwa-9.5.0.tgz#7d70dec2f6b44ace19695c32edf1501028d2bfe1" + integrity sha512-+c+Ni4A51Y+W3MWNG5l6OO629kfByEdiVR8TdqN2I3/cuFXzLzRwgKZZvXKV4ilemrzWm844hny5lP81RG18/Q== + dependencies: + clean-webpack-plugin "4.0.0" + fast-glob "3.3.1" + semver "7.5.4" + terser-webpack-plugin "5.3.9" + workbox-build "7.0.0" + workbox-core "7.0.0" + workbox-webpack-plugin "7.0.0" + workbox-window "7.0.0" + "@emotion/babel-plugin@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" @@ -3025,12 +3039,12 @@ "@firebase/util" "1.9.3" tslib "^2.1.0" -"@firebase/app-compat@0.2.15": - version "0.2.15" - resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.15.tgz#06a932311d340dd94666b9e9cb15ca5fc8bdc434" - integrity sha512-ttEbOEtO1SSz27cRPrwXAmrqDjdQ33sQc7rqqQuSMUuPRdYCQEcYdqzpkbvqgdkzGksx2kfH4JqQ6R/hI12nDw== +"@firebase/app-compat@0.2.18": + version "0.2.18" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.18.tgz#27c89c5f103d31f222a9ba3b30d316841d0fac4a" + integrity sha512-zUbAAZHhwmMUyaNFiFr+1Z/sfcxSQBFrRhpjzzpQMTfiV2C/+P0mC3BQA0HsysdGSYOlwrCs5rEGOyarhRU9Kw== dependencies: - "@firebase/app" "0.9.15" + "@firebase/app" "0.9.18" "@firebase/component" "0.6.4" "@firebase/logger" "0.4.0" "@firebase/util" "1.9.3" @@ -3041,10 +3055,10 @@ resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e" integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== -"@firebase/app@0.9.15": - version "0.9.15" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.15.tgz#8c5b7a85c6f856f3292c1fcc2a029c12a63b9ad9" - integrity sha512-xxQi6mkhRjtXeFUwleSF4zU7lwEH+beNhLE7VmkzEzjEsjAS14QPQPZ35gpgSD+/NigOeho7wgEXd4C/bOkRfA== +"@firebase/app@0.9.18": + version "0.9.18" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.18.tgz#cd8968f5165dceda00759b2b40722ee1d238db93" + integrity sha512-SIJi3B/LzNezaEgbFCFIem12+51khkA3iewYljPQPUArWGSAr1cO9NK8TvtJWax5GMKSmQbJPqgi6a+gxHrWGQ== dependencies: "@firebase/component" "0.6.4" "@firebase/logger" "0.4.0" @@ -3052,12 +3066,12 @@ idb "7.1.1" tslib "^2.1.0" -"@firebase/auth-compat@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.4.4.tgz#062dd397a508c7a442f36c014133ded4d29c62bb" - integrity sha512-B2DctJDJ05djBwebNEdC3zbKWzKdIdxpbca8u9P/NSjqaJNSFq3fhz8h8bjlS9ufSrxaQWFSJMMH3dRmx3FlEA== +"@firebase/auth-compat@0.4.6": + version "0.4.6" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.4.6.tgz#413568be48d23a17aa14438b8aad86556bd1e132" + integrity sha512-pKp1d4fSf+yoy1EBjTx9ISxlunqhW0vTICk0ByZ3e+Lp6ZIXThfUy4F1hAJlEafD/arM0oepRiAh7LXS1xn/BA== dependencies: - "@firebase/auth" "1.1.0" + "@firebase/auth" "1.3.0" "@firebase/auth-types" "0.12.0" "@firebase/component" "0.6.4" "@firebase/util" "1.9.3" @@ -3074,15 +3088,14 @@ resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.0.tgz#f28e1b68ac3b208ad02a15854c585be6da3e8e79" integrity sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA== -"@firebase/auth@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.1.0.tgz#106cad08f977245e641642ac9b7c3a2dae46400d" - integrity sha512-5RJQMXG0p/tSvtqpfM8jA+heELjVCgHHASq3F7NglAa/CWUGCAE4g2F4YDPW5stDkvtKKRez0WYAWnbcuQ5P4w== +"@firebase/auth@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.3.0.tgz#514d77309fdef5cc0ae81d5f57cb07bdaf6822d7" + integrity sha512-vjK4CHbY9aWdiVOrKi6mpa8z6uxeaf7LB/MZTHuZOiGHMcUoTGB6TeMbRShyqk1uaMrxhhZ5Ar/dR0965E1qyA== dependencies: "@firebase/component" "0.6.4" "@firebase/logger" "0.4.0" "@firebase/util" "1.9.3" - "@react-native-async-storage/async-storage" "^1.18.1" node-fetch "2.6.7" tslib "^2.1.0" @@ -3126,13 +3139,13 @@ faye-websocket "0.11.4" tslib "^2.1.0" -"@firebase/firestore-compat@0.3.14": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.14.tgz#f1ceac5a85da52c6b5b9d65136456e3eaec7b227" - integrity sha512-sOjaYefSPXJXdFH6qyxSwJVakEqAAote6jjrJk/ZCoiX90rs9r3yYV90wP4gmaTKyXjkt8EMlwuapekgGsE5Tw== +"@firebase/firestore-compat@0.3.17": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.17.tgz#84e0bf090879d83847ac4af5d1c2820041afb551" + integrity sha512-Qh3tbE4vkn9XvyWnRaJM/v4vhCZ+btk2RZcq037o6oECHohaCFortevd/SKA4vA5yOx0/DwARIEv6XwgHkVucg== dependencies: "@firebase/component" "0.6.4" - "@firebase/firestore" "4.1.0" + "@firebase/firestore" "4.1.3" "@firebase/firestore-types" "3.0.0" "@firebase/util" "1.9.3" tslib "^2.1.0" @@ -3142,17 +3155,17 @@ resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.0.tgz#f3440d5a1cc2a722d361b24cefb62ca8b3577af3" integrity sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw== -"@firebase/firestore@4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.1.0.tgz#fcdd4e033c258fccbe4d47dadf625faa1f62272f" - integrity sha512-FEd+4R0QL9RAJVcdqXgbdIuQYpvzkeKNBVxNM5qcWDPMurjNpja8VaWpVZmT3JXG8FfO+NGTnHJtsW/nWO7XiQ== +"@firebase/firestore@4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.1.3.tgz#7ac530885ccb68c256c98737cfeed41e46d26f79" + integrity sha512-3kw/oZrLAIHuSDTAlKguZ1e0hAgWgiBl4QQm2mIPBvBAs++iEkuv9DH2tr6rbYpT6dWtdn6jj0RN0XeqOouJRg== dependencies: "@firebase/component" "0.6.4" "@firebase/logger" "0.4.0" "@firebase/util" "1.9.3" - "@firebase/webchannel-wrapper" "0.10.1" + "@firebase/webchannel-wrapper" "0.10.2" "@grpc/grpc-js" "~1.8.17" - "@grpc/proto-loader" "^0.6.13" + "@grpc/proto-loader" "^0.7.8" node-fetch "2.6.7" tslib "^2.1.0" @@ -3334,10 +3347,10 @@ dependencies: tslib "^2.1.0" -"@firebase/webchannel-wrapper@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz#60bb2aaf129f9e00621f8d698722ddba6ee1f8ac" - integrity sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw== +"@firebase/webchannel-wrapper@0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.2.tgz#6d7929bf6f5f049418781c2cd9ff3fb0463eac51" + integrity sha512-xDxhD9++451HuCv3xKBEdSYaArX9NcokODXZYH/MxGw1XFFOz7OKkTRItZ5wf6npBN/inwp8dUZCP7SpAg46yQ== "@formatjs/ecma402-abstract@1.11.4": version "1.11.4" @@ -3386,25 +3399,13 @@ "@grpc/proto-loader" "^0.7.0" "@types/node" ">=12.12.47" -"@grpc/proto-loader@^0.6.13": - version "0.6.13" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" - integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== - dependencies: - "@types/long" "^4.0.1" - lodash.camelcase "^4.3.0" - long "^4.0.0" - protobufjs "^6.11.3" - yargs "^16.2.0" - -"@grpc/proto-loader@^0.7.0": - version "0.7.8" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.8.tgz#c050bbeae5f000a1919507f195a1b094e218036e" - integrity sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA== +"@grpc/proto-loader@^0.7.0", "@grpc/proto-loader@^0.7.8": + version "0.7.9" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.9.tgz#3ca68236f1a0d77566dafa53c715eb31d096279a" + integrity sha512-YJsOehVXzgurc+lLAxYnlSMc1p/Gu6VAvnfx0ATi2nzvr0YZcjhmZDeY8SeAKv1M7zE3aEJH0Xo9mK1iZ8GYoQ== dependencies: - "@types/long" "^4.0.1" lodash.camelcase "^4.3.0" - long "^4.0.0" + long "^5.0.0" protobufjs "^7.2.4" yargs "^17.7.2" @@ -4401,13 +4402,6 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@react-native-async-storage/async-storage@^1.18.1": - version "1.19.1" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.19.1.tgz#09d35caaa31823b40fdfeebf95decf8f992a6274" - integrity sha512-5QXuGCtB+HL3VtKL2JN3+6t4qh8VXizK+aGDAv6Dqiq3MLrzgZHb4tjVgtEWMd8CcDtD/JqaAI1b6/EaYGtFIA== - dependencies: - merge-options "^3.0.4" - "@reduxjs/toolkit@^1.9.5": version "1.9.5" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" @@ -5331,6 +5325,27 @@ dependencies: "@types/node" "*" +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.2" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a" + integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -5420,7 +5435,7 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": +"@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== @@ -5452,11 +5467,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== -"@types/long@^4.0.1": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" - integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== - "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -6339,6 +6349,142 @@ joi "17.9.1" rxjs "^7.5.2" +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -6392,6 +6538,11 @@ acorn-globals@^7.0.0: acorn "^8.1.0" acorn-walk "^8.0.2" +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -6412,7 +6563,7 @@ acorn@^8.0.4, acorn@^8.4.1, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== -acorn@^8.1.0, acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== @@ -6781,16 +6932,6 @@ babel-jest@^29.6.4: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@^8.2.5: - version "8.3.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" - integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -6966,11 +7107,6 @@ big-integer@^1.6.48: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - bigint-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" @@ -7189,6 +7325,16 @@ browserify-sign@^4.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +browserslist@^4.14.5, browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + browserslist@^4.21.3, browserslist@^4.21.4: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" @@ -7199,16 +7345,6 @@ browserslist@^4.21.3, browserslist@^4.21.4: node-releases "^2.0.6" update-browserslist-db "^1.0.9" -browserslist@^4.21.9: - version "4.21.10" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" - integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== - dependencies: - caniuse-lite "^1.0.30001517" - electron-to-chromium "^1.4.477" - node-releases "^2.0.13" - update-browserslist-db "^1.0.11" - bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -7458,7 +7594,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -7503,11 +7639,21 @@ chownr@^1.1.4: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + ci-info@^3.2.0: version "3.7.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== +ci-info@^3.7.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + cids@^0.7.1: version "0.7.5" resolved "https://registry.yarnpkg.com/cids/-/cids-0.7.5.tgz#60a08138a99bfb69b6be4ceb63bfef7a396b28b2" @@ -7547,7 +7693,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-webpack-plugin@^4.0.0: +clean-webpack-plugin@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== @@ -7612,15 +7758,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -7748,11 +7885,6 @@ common-tags@^1.8.0: resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -8558,11 +8690,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - encode-utf8@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" @@ -8588,6 +8715,14 @@ enhanced-resolve@^5.10.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -8664,6 +8799,11 @@ es-get-iterator@^1.1.2: is-string "^1.0.5" isarray "^2.0.5" +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -8901,7 +9041,7 @@ eslint-rule-composer@^0.3.0: resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== -eslint-scope@^5.1.1: +eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -9495,7 +9635,7 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0, events@^3.3.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -9681,6 +9821,17 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-glob@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -9803,15 +9954,6 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" -find-cache-dir@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - find-replace@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" @@ -9847,24 +9989,31 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -firebase@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.1.0.tgz#07281ac2fe4bcf3886eeddcea8903ad17f1aec67" - integrity sha512-ghcdCe2G9DeGmLOrBgR7XPswuc9BFUfjnU93ABopIisMfbJFzoqpSp4emwNiZt+vVGZV1ifeU3DLfhxlujxhCg== +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + +firebase@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.3.1.tgz#9e8133866af36f8fe2133fc86b821f2d10dc748b" + integrity sha512-lUk1X0SQocShyIwz5x9mj829Yn1y4Y9KWriGLZ0/Pbwqt4ZxElx8rI1p/YAi4MZTtT1qi0wazo7dAlmuF6J0Aw== dependencies: "@firebase/analytics" "0.10.0" "@firebase/analytics-compat" "0.2.6" - "@firebase/app" "0.9.15" + "@firebase/app" "0.9.18" "@firebase/app-check" "0.8.0" "@firebase/app-check-compat" "0.3.7" - "@firebase/app-compat" "0.2.15" + "@firebase/app-compat" "0.2.18" "@firebase/app-types" "0.9.0" - "@firebase/auth" "1.1.0" - "@firebase/auth-compat" "0.4.4" + "@firebase/auth" "1.3.0" + "@firebase/auth-compat" "0.4.6" "@firebase/database" "1.0.1" "@firebase/database-compat" "1.0.1" - "@firebase/firestore" "4.1.0" - "@firebase/firestore-compat" "0.3.14" + "@firebase/firestore" "4.1.3" + "@firebase/firestore-compat" "0.3.17" "@firebase/functions" "0.10.0" "@firebase/functions-compat" "0.3.5" "@firebase/installations" "0.6.4" @@ -9969,7 +10118,7 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1, fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -10169,7 +10318,7 @@ globalyzer@0.1.0: resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== -globby@^11.0.4, globby@^11.1.0: +globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -10251,6 +10400,11 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" +graceful-fs@^4.1.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -10829,11 +10983,6 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -10934,7 +11083,7 @@ is-what@^3.14.1: resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -11609,7 +11758,7 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-even-better-errors@^2.3.0: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -11674,7 +11823,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: +json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -11778,6 +11927,13 @@ keyvaluestorage-interface@^1.0.0: resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff" integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -11937,14 +12093,10 @@ lit@2.7.6: lit-element "^3.3.0" lit-html "^2.7.0" -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== locate-path@^3.0.0: version "3.0.0" @@ -12122,7 +12274,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -12191,13 +12343,6 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== -merge-options@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" - integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== - dependencies: - is-plain-obj "^2.1.0" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -12227,7 +12372,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -12248,7 +12393,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -12494,24 +12639,17 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -next-pwa@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4" - integrity sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A== - dependencies: - babel-loader "^8.2.5" - clean-webpack-plugin "^4.0.0" - globby "^11.0.4" - terser-webpack-plugin "^5.3.3" - workbox-webpack-plugin "^6.5.4" - workbox-window "^6.5.4" +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== next-tick@1, next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@^13.2.0: +next@^13.4.19: version "13.4.19" resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== @@ -12725,6 +12863,14 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -12768,6 +12914,11 @@ os-shim@^0.1.2: resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" integrity sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A== +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" @@ -12890,6 +13041,27 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -13033,7 +13205,7 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -13251,25 +13423,6 @@ protobufjs@6.11.3, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" -protobufjs@^6.11.3: - version "6.11.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" - integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" ">=13.7.0" - long "^4.0.0" - proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -14189,16 +14342,7 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: +schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -14245,6 +14389,13 @@ semaphore@>=1.0.1, semaphore@^1.0.3: resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== +semver@7.5.4, semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -14267,13 +14418,6 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2: dependencies: lru-cache "^6.0.0" -semver@^7.5.3, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -14427,6 +14571,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -14905,7 +15054,7 @@ table-layout@^1.0.2: typical "^5.2.0" wordwrapjs "^4.0.0" -tapable@^2.2.0: +tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== @@ -14938,7 +15087,7 @@ tempy@^0.6.0: type-fest "^0.16.0" unique-string "^2.0.0" -terser-webpack-plugin@^5.3.3: +terser-webpack-plugin@5.3.9, terser-webpack-plugin@^5.3.7: version "5.3.9" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== @@ -15043,6 +15192,13 @@ tiny-secp256k1@^1.1.6: elliptic "^6.4.0" nan "^2.13.2" +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -15654,7 +15810,7 @@ warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@2.4.0: +watchpack@2.4.0, watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== @@ -15963,6 +16119,41 @@ webpack-sources@^1.4.3: source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.2: + version "5.88.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" + integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + webrtc-adapter@^7.2.1: version "7.7.1" resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz#b2c227a6144983b35057df67bd984a7d4bfd17f1" @@ -16127,14 +16318,6 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" -workbox-background-sync@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" - integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== - dependencies: - idb "^7.0.1" - workbox-core "6.6.1" - workbox-background-sync@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5" @@ -16143,13 +16326,6 @@ workbox-background-sync@7.0.0: idb "^7.0.1" workbox-core "7.0.0" -workbox-broadcast-update@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" - integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== - dependencies: - workbox-core "6.6.1" - workbox-broadcast-update@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22" @@ -16157,49 +16333,6 @@ workbox-broadcast-update@7.0.0: dependencies: workbox-core "7.0.0" -workbox-build@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" - integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== - dependencies: - "@apideck/better-ajv-errors" "^0.3.1" - "@babel/core" "^7.11.1" - "@babel/preset-env" "^7.11.0" - "@babel/runtime" "^7.11.2" - "@rollup/plugin-babel" "^5.2.0" - "@rollup/plugin-node-resolve" "^11.2.1" - "@rollup/plugin-replace" "^2.4.1" - "@surma/rollup-plugin-off-main-thread" "^2.2.3" - ajv "^8.6.0" - common-tags "^1.8.0" - fast-json-stable-stringify "^2.1.0" - fs-extra "^9.0.1" - glob "^7.1.6" - lodash "^4.17.20" - pretty-bytes "^5.3.0" - rollup "^2.43.1" - rollup-plugin-terser "^7.0.0" - source-map "^0.8.0-beta.0" - stringify-object "^3.3.0" - strip-comments "^2.0.1" - tempy "^0.6.0" - upath "^1.2.0" - workbox-background-sync "6.6.1" - workbox-broadcast-update "6.6.1" - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-google-analytics "6.6.1" - workbox-navigation-preload "6.6.1" - workbox-precaching "6.6.1" - workbox-range-requests "6.6.1" - workbox-recipes "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-streams "6.6.1" - workbox-sw "6.6.1" - workbox-window "6.6.1" - workbox-build@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4" @@ -16243,13 +16376,6 @@ workbox-build@7.0.0: workbox-sw "7.0.0" workbox-window "7.0.0" -workbox-cacheable-response@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" - integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== - dependencies: - workbox-core "6.6.1" - workbox-cacheable-response@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2" @@ -16257,24 +16383,11 @@ workbox-cacheable-response@7.0.0: dependencies: workbox-core "7.0.0" -workbox-core@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" - integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== - workbox-core@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545" integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ== -workbox-expiration@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" - integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== - dependencies: - idb "^7.0.1" - workbox-core "6.6.1" - workbox-expiration@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa" @@ -16283,16 +16396,6 @@ workbox-expiration@7.0.0: idb "^7.0.1" workbox-core "7.0.0" -workbox-google-analytics@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" - integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== - dependencies: - workbox-background-sync "6.6.1" - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-google-analytics@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a" @@ -16303,13 +16406,6 @@ workbox-google-analytics@7.0.0: workbox-routing "7.0.0" workbox-strategies "7.0.0" -workbox-navigation-preload@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" - integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== - dependencies: - workbox-core "6.6.1" - workbox-navigation-preload@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c" @@ -16317,15 +16413,6 @@ workbox-navigation-preload@7.0.0: dependencies: workbox-core "7.0.0" -workbox-precaching@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" - integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== - dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-precaching@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03" @@ -16335,13 +16422,6 @@ workbox-precaching@7.0.0: workbox-routing "7.0.0" workbox-strategies "7.0.0" -workbox-range-requests@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" - integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== - dependencies: - workbox-core "6.6.1" - workbox-range-requests@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed" @@ -16349,18 +16429,6 @@ workbox-range-requests@7.0.0: dependencies: workbox-core "7.0.0" -workbox-recipes@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" - integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== - dependencies: - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-precaching "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-recipes@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514" @@ -16373,13 +16441,6 @@ workbox-recipes@7.0.0: workbox-routing "7.0.0" workbox-strategies "7.0.0" -workbox-routing@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" - integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== - dependencies: - workbox-core "6.6.1" - workbox-routing@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302" @@ -16387,13 +16448,6 @@ workbox-routing@7.0.0: dependencies: workbox-core "7.0.0" -workbox-strategies@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf" - integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw== - dependencies: - workbox-core "6.6.1" - workbox-strategies@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382" @@ -16401,14 +16455,6 @@ workbox-strategies@7.0.0: dependencies: workbox-core "7.0.0" -workbox-streams@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" - integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== - dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-streams@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9" @@ -16417,28 +16463,12 @@ workbox-streams@7.0.0: workbox-core "7.0.0" workbox-routing "7.0.0" -workbox-sw@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" - integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== - workbox-sw@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86" integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA== -workbox-webpack-plugin@^6.5.4: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" - integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== - dependencies: - fast-json-stable-stringify "^2.1.0" - pretty-bytes "^5.4.1" - upath "^1.2.0" - webpack-sources "^1.4.3" - workbox-build "6.6.1" - -workbox-webpack-plugin@^7.0.0: +workbox-webpack-plugin@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55" integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw== @@ -16449,14 +16479,6 @@ workbox-webpack-plugin@^7.0.0: webpack-sources "^1.4.3" workbox-build "7.0.0" -workbox-window@6.6.1, workbox-window@^6.5.4: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" - integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== - dependencies: - "@types/trusted-types" "^2.0.2" - workbox-core "6.6.1" - workbox-window@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08" @@ -16638,6 +16660,11 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" + integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -16654,11 +16681,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -16697,19 +16719,6 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.3.1: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" From 8d33c983633668020229b9a54f8f4ca082877df7 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 14:31:35 +0200 Subject: [PATCH 35/62] fix: `next-pwa` with custom worker --- next.config.mjs | 22 +- package.json | 9 +- patches/@ducanh2912+next-pwa+9.5.0.patch | 24 + .../settings/PushNotifications/logic.ts | 6 +- src/hooks/useFirebaseNotifications.ts | 19 - src/pages/_app.tsx | 2 - .../firebase-messaging-sw.ts} | 19 +- src/service-workers/index.ts | 7 + src/services/firebase/app.ts | 24 +- src/services/firebase/notifications.ts | 3 +- tsconfig.json | 3 +- yarn.lock | 705 ++++++++++++------ 12 files changed, 546 insertions(+), 297 deletions(-) create mode 100644 patches/@ducanh2912+next-pwa+9.5.0.patch delete mode 100644 src/hooks/useFirebaseNotifications.ts rename src/{worker/index.ts => service-workers/firebase-messaging-sw.ts} (74%) create mode 100644 src/service-workers/index.ts diff --git a/next.config.mjs b/next.config.mjs index a1badd924e..5e568cd377 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,18 +2,20 @@ import path from 'path' import withBundleAnalyzer from '@next/bundle-analyzer' import withPWAInit from '@ducanh2912/next-pwa' +const SERVICE_WORKERS_PATH = './src/service-workers' + const withPWA = withPWAInit({ dest: 'public', - // reloadOnOnline: false, - // /* Do not precache anything */ - // publicExcludes: ['**/*'], - // buildExcludes: [/./], - // InjectManifest for Web Push - swSrc: './src/worker/index.ts', - // register: false, - // workboxOptions: { - // mode: 'production', - // }, + workboxOptions: { + mode: 'production', + }, + reloadOnOnline: false, + /* Do not precache anything */ + publicExcludes: ['**/*'], + buildExcludes: [/./], + customWorkerSrc: SERVICE_WORKERS_PATH, + // Prefer InjectManifest for Web Push + swSrc: `${SERVICE_WORKERS_PATH}/index.ts`, }) /** @type {import('next').NextConfig} */ diff --git a/package.json b/package.json index 8fec0317e9..0bc4f6a45d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts", "css-vars": "ts-node-esm ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css", "generate-types": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@safe-global/safe-deployments/dist/assets/**/*.json ./node_modules/@safe-global/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC721.json", - "postinstall": "yarn generate-types && yarn css-vars", + "postinstall": "yarn patch-package && yarn generate-types && yarn css-vars", "analyze": "cross-env ANALYZE=true yarn build", "cypress:open": "cross-env TZ=UTC cypress open --e2e", "cypress:canary": "cross-env TZ=UTC cypress open --e2e -b chrome:canary", @@ -88,8 +88,7 @@ "react-papaparse": "^4.0.2", "react-qr-reader": "2.2.1", "react-redux": "^8.0.5", - "semver": "^7.5.2", - "webpack": "^5.88.2" + "semver": "^7.5.2" }, "devDependencies": { "@next/bundle-analyzer": "^13.1.1", @@ -124,12 +123,14 @@ "fake-indexeddb": "^4.0.2", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", + "patch-package": "^8.0.0", "pre-commit": "^1.2.2", "prettier": "^2.7.0", "ts-node": "^10.8.2", "ts-prune": "^0.10.3", "typechain": "^8.0.0", "typescript": "4.9.4", - "typescript-plugin-css-modules": "^4.2.2" + "typescript-plugin-css-modules": "^4.2.2", + "webpack": "^5.88.2" } } diff --git a/patches/@ducanh2912+next-pwa+9.5.0.patch b/patches/@ducanh2912+next-pwa+9.5.0.patch new file mode 100644 index 0000000000..4daf23e504 --- /dev/null +++ b/patches/@ducanh2912+next-pwa+9.5.0.patch @@ -0,0 +1,24 @@ ++ Allow Webpack to resolve ECMAScript modules without explicit extensions ++ Currently required for firebase to compile in next-pwa custom worker ++ https://webpack.js.org/configuration/module/#resolvefullyspecified + +diff --git a/node_modules/@ducanh2912/next-pwa/dist/index.cjs b/node_modules/@ducanh2912/next-pwa/dist/index.cjs +index 3a9d49b..08ab877 100644 +--- a/node_modules/@ducanh2912/next-pwa/dist/index.cjs ++++ b/node_modules/@ducanh2912/next-pwa/dist/index.cjs +@@ -1,2 +1,2 @@ + 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var r=require('path'),url=require('url'),module$1=require('module'),e$2=require('fs'),n$1=require('process'),l=require('os'),i=require('tty'),semver=require('semver'),cleanWebpackPlugin=require('clean-webpack-plugin'),s$1=require('fast-glob'),t$1=require('workbox-webpack-plugin'),e$3=require('crypto'),n$2=require('webpack'),s=require('terser-webpack-plugin');let e$1;let a$1=(e,r,t)=>{e.jsc||(e.jsc={}),e.jsc.baseUrl=r,e.jsc.paths=t;},c=(e,r)=>{for(let t of e){let e=r(t);if(e)return e}},u=module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))),p$1=e=>{try{return u(`${e}/package.json`).version}catch{return}},f=(e,r$1)=>{try{let n=c([r$1??"tsconfig.json","jsconfig.json"],r$1=>{let n=r.join(e,r$1);return e$2.existsSync(n)?n:void 0});if(!n)return;return JSON.parse(e$2.readFileSync(n,"utf-8"))}catch{return}},m$1=(e=0)=>r=>`\u001B[${r+e}m`,b=(e=0)=>r=>`\u001B[${38+e};5;${r}m`,g=(e=0)=>(r,t,o)=>`\u001B[${38+e};2;${r};${t};${o}m`,h={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(h.modifier);let d$1=Object.keys(h.color),O=Object.keys(h.bgColor);[...d$1,...O];let y=function(){let e=new Map;for(let[r,t]of Object.entries(h)){for(let[r,o]of Object.entries(t))h[r]={open:`\u001B[${o[0]}m`,close:`\u001B[${o[1]}m`},t[r]=h[r],e.set(o[0],o[1]);Object.defineProperty(h,r,{value:t,enumerable:!1});}return Object.defineProperty(h,"codes",{value:e,enumerable:!1}),h.color.close="\x1b[39m",h.bgColor.close="\x1b[49m",h.color.ansi=m$1(),h.color.ansi256=b(),h.color.ansi16m=g(),h.bgColor.ansi=m$1(10),h.bgColor.ansi256=b(10),h.bgColor.ansi16m=g(10),Object.defineProperties(h,{rgbToAnsi256:{value:(e,r,t)=>e===r&&r===t?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(r/255*5)+Math.round(t/255*5),enumerable:!1},hexToRgb:{value(e){let r=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!r)return [0,0,0];let[t]=r;3===t.length&&(t=[...t].map(e=>e+e).join(""));let o=Number.parseInt(t,16);return [o>>16&255,o>>8&255,255&o]},enumerable:!1},hexToAnsi256:{value:e=>h.rgbToAnsi256(...h.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){let r,t,o;if(e<8)return 30+e;if(e<16)return 90+(e-8);if(e>=232)t=r=((e-232)*10+8)/255,o=r;else {let n=(e-=16)%36;r=Math.floor(e/36)/5,t=Math.floor(n/6)/5,o=n%6/5;}let n=2*Math.max(r,t,o);if(0===n)return 30;let l=30+(Math.round(o)<<2|Math.round(t)<<1|Math.round(r));return 2===n&&(l+=60),l},enumerable:!1},rgbToAnsi:{value:(e,r,t)=>h.ansi256ToAnsi(h.rgbToAnsi256(e,r,t)),enumerable:!1},hexToAnsi:{value:e=>h.ansi256ToAnsi(h.hexToAnsi256(e)),enumerable:!1}}),h}();function v(e,r=globalThis.Deno?globalThis.Deno.args:n$1.argv){let t=e.startsWith("-")?"":1===e.length?"-":"--",o=r.indexOf(t+e),l=r.indexOf("--");return -1!==o&&(-1===l||o=10&&Number(e[2])>=10586?Number(e[2])>=14931?3:2:1}if("CI"in T)return "GITHUB_ACTIONS"in T||"GITEA_ACTIONS"in T?3:["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(e=>e in T)||"codeship"===T.CI_NAME?1:a;if("TEAMCITY_VERSION"in T)return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(T.TEAMCITY_VERSION)?1:0;if("truecolor"===T.COLORTERM||"xterm-kitty"===T.TERM)return 3;if("TERM_PROGRAM"in T){let e=Number.parseInt((T.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(T.TERM_PROGRAM){case"iTerm.app":return e>=3?3:2;case"Apple_Terminal":return 2}}return /-256(color)?$/i.test(T.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(T.TERM)||"COLORTERM"in T?1:a}(r,{streamIsTTY:r&&r.isTTY,...t});return 0!==o&&{level:o,hasBasic:!0,has256:o>=2,has16m:o>=3}}v("no-color")||v("no-colors")||v("color=false")||v("color=never")?e$1=0:(v("color")||v("colors")||v("color=true")||v("color=always"))&&(e$1=1);let{stdout:j,stderr:R}={stdout:M({isTTY:i.isatty(1)}),stderr:M({isTTY:i.isatty(2)})},w=Symbol("GENERATOR"),C=Symbol("STYLER"),A=Symbol("IS_EMPTY"),E=["ansi","ansi","ansi256","ansi16m"],B=Object.create(null),S=(e,r={})=>{if(r.level&&!(Number.isInteger(r.level)&&r.level>=0&&r.level<=3))throw Error("The `level` option should be an integer from 0 to 3");let t=j?j.level:0;e.level=void 0===r.level?t:r.level;},x=e=>{let r=(...e)=>e.join(" ");return S(r,e),Object.setPrototypeOf(r,I.prototype),r};function I(e){return x(e)}for(let[e,r]of(Object.setPrototypeOf(I.prototype,Function.prototype),Object.entries(y)))B[e]={get(){let t=$(this,_(r.open,r.close,this[C]),this[A]);return Object.defineProperty(this,e,{value:t}),t}};B.visible={get(){let e=$(this,this[C],!0);return Object.defineProperty(this,"visible",{value:e}),e}};let P=(e,r,t,...o)=>"rgb"===e?"ansi16m"===r?y[t].ansi16m(...o):"ansi256"===r?y[t].ansi256(y.rgbToAnsi256(...o)):y[t].ansi(y.rgbToAnsi(...o)):"hex"===e?P("rgb",r,t,...y.hexToRgb(...o)):y[t][e](...o);for(let e of ["rgb","hex","ansi256"])B[e]={get(){let{level:r}=this;return function(...t){return $(this,_(P(e,E[r],"color",...t),y.color.close,this[C]),this[A])}}},B["bg"+e[0].toUpperCase()+e.slice(1)]={get(){let{level:r}=this;return function(...t){return $(this,_(P(e,E[r],"bgColor",...t),y.bgColor.close,this[C]),this[A])}}};let N=Object.defineProperties(()=>{},{...B,level:{enumerable:!0,get(){return this[w].level},set(e){this[w].level=e;}}}),_=(e,r,t)=>{let o,n;return void 0===t?(o=e,n=r):(o=t.openAll+e,n=r+t.closeAll),{open:e,close:r,openAll:o,closeAll:n,parent:t}},$=(e,r,t)=>{let o=(...e)=>k(o,1===e.length?""+e[0]:e.join(" "));return Object.setPrototypeOf(o,N),o[w]=e,o[C]=r,o[A]=t,o},k=(e,r)=>{if(e.level<=0||!r)return e[A]?"":r;let t=e[C];if(void 0===t)return r;let{openAll:o,closeAll:n}=t;if(r.includes("\x1b"))for(;void 0!==t;)r=function(e,r,t){let o=e.indexOf(r);if(-1===o)return e;let n=r.length,l=0,i="";do i+=e.slice(l,o)+r+t,l=o+n,o=e.indexOf(r,l);while(-1!==o)return i+e.slice(l)}(r,t.close,t.open),t=t.parent;let l=r.indexOf("\n");return -1!==l&&(r=function(e,r,t,o){let n=0,l="";do{let i="\r"===e[o-1];l+=e.slice(n,i?o-1:o)+r+(i?"\r\n":"\n")+t,n=o+1,o=e.indexOf("\n",n);}while(-1!==o)return l+e.slice(n)}(r,n,o,l)),o+r+n};Object.defineProperties(I.prototype,B);let L=x(void 0);x({level:R?R.level:0});let F=p$1("next"),G=!!F&&semver.gte(F,"13.4.1"),Y=(e,r=0)=>G?`- ${e} (pwa)`:`${e}${" ".repeat(r)}- (PWA)`,D={wait:Y(L.cyan("wait"),2),error:Y(L.red("error"),1),warn:Y(L.yellow("warn"),2),info:Y(L.cyan("info"),2)};var V=Object.freeze({__proto__:null,error:(...e)=>{console.error(D.error,...e);},info:(...e)=>{console.log(D.info,...e);},prefixes:D,wait:(...e)=>{console.log(D.wait,...e);},warn:(...e)=>{console.warn(D.warn,...e);}});let J=()=>{let e;for(let r of ["@swc/core","next/dist/build/swc"])try{e=require(r);break}catch{}if(!e)throw Error("Failed to resolve swc. Please install @swc/core if you haven't.");return e};module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)));let q=async(e,r,t,o)=>{let{resolveSwc:n,useSwcMinify:l,...i}=t,s=()=>require("terser-webpack-plugin").terserMinify(e,r,i,o);if(l){let t,o;try{t=n();}catch{return s()}if(!t.minify)return s();let l={...i,compress:"boolean"==typeof i.compress?!!i.compress&&{}:{...i.compress},mangle:null==i.mangle||("boolean"==typeof i.mangle?i.mangle:{...i.mangle}),sourceMap:void 0};r&&(l.sourceMap=!0),l.compress&&(void 0===l.compress.ecma&&(l.compress.ecma=l.ecma),5===l.ecma&&void 0===l.compress.arrows&&(l.compress.arrows=!1));let[[a,c]]=Object.entries(e),u=await t.minify(c,l);return u.map&&((o=JSON.parse(u.map)).sources=[a],delete o.sourcesContent),{code:u.code,map:o}}return s()};var t = [{urlPattern:/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,handler:"CacheFirst",options:{cacheName:"google-fonts-webfonts",expiration:{maxEntries:4,maxAgeSeconds:31536e3}}},{urlPattern:/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,handler:"StaleWhileRevalidate",options:{cacheName:"google-fonts-stylesheets",expiration:{maxEntries:4,maxAgeSeconds:604800}}},{urlPattern:/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-font-assets",expiration:{maxEntries:4,maxAgeSeconds:604800}}},{urlPattern:/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-image-assets",expiration:{maxEntries:64,maxAgeSeconds:2592e3}}},{urlPattern:/\/_next\/static.+\.js$/i,handler:"CacheFirst",options:{cacheName:"next-static-js-assets",expiration:{maxEntries:64,maxAgeSeconds:86400}}},{urlPattern:/\/_next\/image\?url=.+$/i,handler:"StaleWhileRevalidate",options:{cacheName:"next-image",expiration:{maxEntries:64,maxAgeSeconds:86400}}},{urlPattern:/\.(?:mp3|wav|ogg)$/i,handler:"CacheFirst",options:{rangeRequests:!0,cacheName:"static-audio-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:mp4)$/i,handler:"CacheFirst",options:{rangeRequests:!0,cacheName:"static-video-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:js)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-js-assets",expiration:{maxEntries:48,maxAgeSeconds:86400}}},{urlPattern:/\.(?:css|less)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-style-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\/_next\/data\/.+\/.+\.json$/i,handler:"StaleWhileRevalidate",options:{cacheName:"next-data",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:json|xml|csv)$/i,handler:"NetworkFirst",options:{cacheName:"static-data-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({sameOrigin:e,url:{pathname:t}})=>!(!e||t.startsWith("/api/auth/"))&&!!t.startsWith("/api/"),handler:"NetworkFirst",method:"GET",options:{cacheName:"apis",expiration:{maxEntries:16,maxAgeSeconds:86400},networkTimeoutSeconds:10}},{urlPattern:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!t.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages-rsc-prefetch",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!t.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages-rsc",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({url:{pathname:e},sameOrigin:t})=>t&&!e.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({sameOrigin:e})=>!e,handler:"NetworkFirst",options:{cacheName:"cross-origin",expiration:{maxEntries:32,maxAgeSeconds:3600},networkTimeoutSeconds:10}}];const resolveWorkboxCommon=({dest:e,sw:a,dev:r$1,buildId:n,buildExcludes:s,manifestEntries:i,manifestTransforms:l,modifyURLPrefix:o,publicPath:m})=>({swDest:r.join(e,a),additionalManifestEntries:r$1?[]:i,exclude:[...s,({asset:t})=>!!(t.name.startsWith("server/")||t.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/))||!!r$1&&!t.name.startsWith("static/runtime/")],modifyURLPrefix:{...o,"/_next/../public/":"/"},manifestTransforms:[...l,async(t,e)=>{let a=t.map(t=>{if(t.url=t.url.replace("/_next//static/image","/_next/static/image"),t.url=t.url.replace("/_next//static/media","/_next/static/media"),null===t.revision){let a=t.url;"string"==typeof m&&a.startsWith(m)&&(a=t.url.substring(m.length));let r=e.assetsInfo.get(a);t.revision=r&&r.contenthash||n;}return t.url=t.url.replace(/\[/g,"%5B").replace(/\]/g,"%5D"),t});return {manifest:a,warnings:[]}}]}); +-const resolveRuntimeCaching=(o,n)=>{if(!o)return t;if(!n)return V.info("Custom runtimeCaching array found, using it instead of the default one."),o;V.info("Custom runtimeCaching array found, using it to extend the default one.");let a=[],i=new Set;for(let e of o)a.push(e),e.options?.cacheName&&i.add(e.options.cacheName);for(let e of t)e.options?.cacheName&&i.has(e.options.cacheName)||a.push(e);return a};const overrideAfterCalledMethod=e=>{Object.defineProperty(e,"alreadyCalled",{get:()=>!1,set(){}});};const isInjectManifestConfig=e=>void 0!==e&&"string"==typeof e.swSrc;const convertBoolean=(e,t=!0)=>{switch(typeof e){case"boolean":return e;case"number":case"bigint":return e>0;case"object":return null!==e;case"string":if(!t){if("false"===e||"0"===e)return !1;return !0}return "true"===e||"1"===e;case"function":case"symbol":return !0;case"undefined":return !1}};const getFileHash=r=>e$3.createHash("md5").update(e$2.readFileSync(r)).digest("hex");const getContentHash=(e,t)=>t?"development":getFileHash(e).slice(0,16);const resolveWorkboxPlugin=({rootDir:s,basePath:a,isDev:p,workboxCommon:l,workboxOptions:c,importScripts:u,extendDefaultRuntimeCaching:d,dynamicStartUrl:h,hasFallbacks:f})=>{if(isInjectManifestConfig(c)){let o=r.join(s,c.swSrc);V.info(`Using InjectManifest with ${o}`);let r$1=new t$1.InjectManifest({...l,...c,swSrc:o});return p&&overrideAfterCalledMethod(r$1),r$1}{let e;let{skipWaiting:r=!0,clientsClaim:s=!0,cleanupOutdatedCaches:m=!0,ignoreURLParametersMatching:g=[],importScripts:w,runtimeCaching:b}=c;w&&u.push(...w);let k=!1;p?(V.info("Building in development mode, caching and precaching are disabled for the most part. This means that offline support is disabled, but you can continue developing other functions in service worker."),g.push(/ts/),e=[{urlPattern:/.*/i,handler:"NetworkOnly",options:{cacheName:"dev"}}],k=!0):e=resolveRuntimeCaching(b,d),h&&e.unshift({urlPattern:a,handler:"NetworkFirst",options:{cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}}),f&&e.forEach(e=>{!e.options||e.options.precacheFallback||e.options.plugins?.find(e=>"handlerDidError"in e)||(e.options.plugins||(e.options.plugins=[]),e.options.plugins.push({handlerDidError:async({request:e})=>"undefined"!=typeof self?self.fallback(e):Response.error()}));});let y=new t$1.GenerateSW({...l,skipWaiting:r,clientsClaim:s,cleanupOutdatedCaches:m,ignoreURLParametersMatching:g,importScripts:u,...c,runtimeCaching:e});return k&&overrideAfterCalledMethod(y),y}};const defaultSwcRc={module:{type:"es6",lazy:!0,noInterop:!0},jsc:{parser:{syntax:"typescript",tsx:!0,dynamicImport:!0,decorators:!1},transform:{react:{runtime:"automatic"}},target:"es2022",loose:!1},minify:!1};let e=(t,e)=>{if(t)return e?.(t)};const NextPWAContext={shouldMinify:e(process.env.NEXT_PWA_MINIFY,convertBoolean),useSwcMinify:e(process.env.NEXT_PWA_SWC_MINIFY,convertBoolean)};const setDefaultContext=(t,e)=>{void 0===NextPWAContext[t]&&(NextPWAContext[t]=e);};let n=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)))),a=()=>({compress:{ecma:5,comparisons:!1,inline:2},mangle:{safari10:!0},format:{ecma:5,safari10:!0,comments:!1,ascii_only:!0},resolveSwc:J,useSwcMinify:NextPWAContext.useSwcMinify});const getSharedWebpackConfig=({swcRc:o=defaultSwcRc})=>{let i=NextPWAContext.shouldMinify&&{minimize:!0,minimizer:[new s({minify:q,terserOptions:a()})]};return {resolve:{extensions:[".js",".ts"],fallback:{module:!1,dgram:!1,dns:!1,path:!1,fs:!1,os:!1,crypto:!1,stream:!1,http2:!1,net:!1,tls:!1,zlib:!1,child_process:!1}},resolveLoader:{alias:{"swc-loader":r.join(n,"swc-loader.cjs")}},module:{rules:[{test:/\.(t|j)s$/i,use:[{loader:"swc-loader",options:o}]}]},optimization:i||void 0}};const buildCustomWorker=({isDev:c$1,baseDir:a,customWorkerSrc:f,customWorkerDest:d,customWorkerPrefix:j,plugins:h=[],tsconfig:w,basePath:k})=>{let $=c([f,r.join("src",f)],t=>{t=r.join(a,t);let e=["ts","js"].map(o=>r.join(t,`index.${o}`)).filter(r=>e$2.existsSync(r));if(0===e.length)return;let n=e[0];return e.length>1&&V.info(`More than one custom worker found, ${n} will be used.`),n});if(!$)return;V.info(`Found a custom worker implementation at ${$}.`),w&&w.compilerOptions&&w.compilerOptions.paths&&a$1(defaultSwcRc,r.join(a,w.compilerOptions.baseUrl??"."),w.compilerOptions.paths);let b=`${j}-${getContentHash($,c$1)}.js`;return V.info(`Building custom worker to ${r.join(d,b)}...`),n$2({...getSharedWebpackConfig({swcRc:defaultSwcRc}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:$},output:{path:d,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(d,`${j}-*.js`),r.join(d,`${j}-*.js.map`)]}),...h]}).run((o,r)=>{(o||r?.hasErrors())&&(V.error("Failed to build custom worker."),V.error(r?.toString({colors:!0})),process.exit(-1));}),r.posix.join(k,b)};const getFallbackEnvs=({fallbacks:L,buildId:e})=>{let t=L.data;t&&t.endsWith(".json")&&(t=r.posix.join("/_next/data",e,t));let o={__PWA_FALLBACK_DOCUMENT__:L.document||!1,__PWA_FALLBACK_IMAGE__:L.image||!1,__PWA_FALLBACK_AUDIO__:L.audio||!1,__PWA_FALLBACK_VIDEO__:L.video||!1,__PWA_FALLBACK_FONT__:L.font||!1,__PWA_FALLBACK_DATA__:t||!1};if(0!==Object.values(o).filter(_=>!!_).length)return V.info("This app will fallback to these precached routes when fetching from the cache and the network fails:"),o.__PWA_FALLBACK_DOCUMENT__&&V.info(` Documents (pages): ${o.__PWA_FALLBACK_DOCUMENT__}`),o.__PWA_FALLBACK_IMAGE__&&V.info(` Images: ${o.__PWA_FALLBACK_IMAGE__}`),o.__PWA_FALLBACK_AUDIO__&&V.info(` Audio: ${o.__PWA_FALLBACK_AUDIO__}`),o.__PWA_FALLBACK_VIDEO__&&V.info(` Videos: ${o.__PWA_FALLBACK_VIDEO__}`),o.__PWA_FALLBACK_FONT__&&V.info(` Fonts: ${o.__PWA_FALLBACK_FONT__}`),o.__PWA_FALLBACK_DATA__&&V.info(` Data (/_next/data/**/*.json): ${o.__PWA_FALLBACK_DATA__}`),o};let m=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));const buildFallbackWorker=({isDev:e,buildId:c,fallbacks:p,destDir:u,basePath:f})=>{p=Object.keys(p).reduce((e,o)=>{let t=p[o];return t&&(e[o]=r.posix.join(f,t)),e},{});let j=getFallbackEnvs({fallbacks:p,buildId:c});if(!j)return;let k=r.join(m,"fallback.js"),b=`fallback-${getContentHash(k,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:k},output:{path:u,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"fallback-*.js"),r.join(u,"fallback-*.js.map")]}),new n$2.EnvironmentPlugin(j)]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build fallback worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),{name:r.posix.join(f,b),precaches:Object.values(j).filter(r=>!!r)}};let p=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));const buildSWEntryWorker=({isDev:e,destDir:u,shouldGenSWEWorker:l,basePath:a})=>{if(!l)return;let w=r.join(p,"sw-entry-worker.js"),c=`swe-worker-${getContentHash(w,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:w},output:{path:u,filename:c,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"swe-worker-*.js"),r.join(u,"swe-worker-*.js.map")]})]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build the service worker's sub-worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),r.posix.join(a,c)};const getDefaultDocumentPage=(t,f,n)=>{let s;let r$1=c(["pages","src/pages"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0));if(n&&(s=c(["app","src/app"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0))),r$1||s)for(let o of f){if(s){let t=r.join(s,`~offline/page.${o}`);if(e$2.existsSync(t))return "/~offline"}if(r$1){let t=r.join(r$1,`_offline.${o}`);if(t&&e$2.existsSync(t))return "/_offline"}}};let d=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));var index = ((e={})=>(t={})=>({...t,webpack(w,j){let h;try{h=require("next/dist/server/config-shared").defaultConfig;}catch{}let b=t.experimental?.appDir??h?.experimental?.appDir??!0,g=j.webpack,{buildId:k,dev:x,config:{distDir:y=".next",pageExtensions:A=["tsx","ts","jsx","js","mdx"]}}=j,v=j.config.basePath||"/",D=f(j.dir,t?.typescript?.tsconfigPath),{disable:P=!1,register:E=!0,dest:W=y,sw:$="sw.js",cacheStartUrl:S=!0,dynamicStartUrl:O=!0,dynamicStartUrlRedirect:R,publicExcludes:N=["!noprecache/**/*"],buildExcludes:C=[],fallbacks:L={},cacheOnFrontEndNav:M=!1,aggressiveFrontEndNavCaching:T=!1,reloadOnOnline:G=!0,scope:B=v,customWorkerDir:U,customWorkerSrc:F=U||"worker",customWorkerDest:H=W,customWorkerPrefix:I="worker",workboxOptions:Y={},extendDefaultRuntimeCaching:q=!1,swcMinify:K=t.swcMinify??h?.swcMinify??!1}=e;if("function"==typeof t.webpack&&(w=t.webpack(w,j)),P)return j.isServer&&V.info("PWA support is disabled."),w;let V$1=[];w.plugins||(w.plugins=[]),V.info(`Compiling for ${j.isServer?"server":"client (static)"}...`);let z=r.posix.join(v,$),J=r.posix.join(B,"/");w.plugins.push(new g.DefinePlugin({__PWA_SW__:`'${z}'`,__PWA_SCOPE__:`'${J}'`,__PWA_ENABLE_REGISTER__:`${!!E}`,__PWA_START_URL__:O?`'${v}'`:void 0,__PWA_CACHE_ON_FRONT_END_NAV__:`${!!M}`,__PWA_AGGRFEN_CACHE__:`${!!T}`,__PWA_RELOAD_ON_ONLINE__:`${!!G}`}));let Q=r.join(d,"sw-entry.js"),X=w.entry;if(w.entry=()=>X().then(i=>(i["main.js"]&&!i["main.js"].includes(Q)&&(Array.isArray(i["main.js"])?i["main.js"].unshift(Q):"string"==typeof i["main.js"]&&(i["main.js"]=[Q,i["main.js"]])),i["main-app"]&&!i["main-app"].includes(Q)&&(Array.isArray(i["main-app"])?i["main-app"].unshift(Q):"string"==typeof i["main-app"]&&(i["main-app"]=[Q,i["main-app"]])),i)),!j.isServer){setDefaultContext("shouldMinify",!x),setDefaultContext("useSwcMinify",K);let e=r.join(j.dir,W),o=r.join(j.dir,H),t=buildSWEntryWorker({isDev:x,destDir:e,shouldGenSWEWorker:M,basePath:v});w.plugins.push(new g.DefinePlugin({__PWA_SW_ENTRY_WORKER__:t&&`'${t}'`})),E||(V.info("Service worker won't be automatically registered as per the config, please call the following code in componentDidMount or useEffect:"),V.info(" window.workbox.register()"),D?.compilerOptions?.types?.includes("@ducanh2912/next-pwa/workbox")||V.info("You may also want to add @ducanh2912/next-pwa/workbox to compilerOptions.types in your tsconfig.json/jsconfig.json.")),V.info(`Service worker: ${r.join(e,$)}`),V.info(` URL: ${z}`),V.info(` Scope: ${J}`),w.plugins.push(new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(e,"workbox-*.js"),r.join(e,"workbox-*.js.map"),r.join(e,$),r.join(e,`${$}.map`),r.join(e,"sw-chunks/**")]}));let d=buildCustomWorker({isDev:x,baseDir:j.dir,swDest:e,customWorkerSrc:F,customWorkerDest:o,customWorkerPrefix:I,plugins:w.plugins.filter(i=>i instanceof g.DefinePlugin),tsconfig:D,basePath:v});d&&V$1.unshift(d);let{additionalManifestEntries:h,modifyURLPrefix:y={},manifestTransforms:P=[],exclude:T,...G}=Y,B=h??[];B||(B=s$1.sync(["**/*","!workbox-*.js","!workbox-*.js.map","!worker-*.js","!worker-*.js.map","!fallback-*.js","!fallback-*.js.map",`!${$.replace(/^\/+/,"")}`,`!${$.replace(/^\/+/,"")}.map`,...N],{cwd:"public"}).map(e=>({url:r.posix.join(v,e),revision:getFileHash(`public/${e}`)}))),S&&(O?"string"==typeof R&&R.length>0&&B.push({url:R,revision:k}):B.push({url:v,revision:k})),Object.keys(G).forEach(i=>void 0===G[i]&&delete G[i]);let U=!1;if(L){L.document||(L.document=getDefaultDocumentPage(j.dir,A,b));let i=buildFallbackWorker({isDev:x,buildId:k,fallbacks:L,destDir:e,basePath:v});i&&(U=!0,V$1.unshift(i.name),i.precaches.forEach(i=>{i&&"boolean"!=typeof i&&!B.find(e=>"string"!=typeof e&&e.url.startsWith(i))&&B.push({url:i,revision:k});}));}let Q=resolveWorkboxCommon({dest:e,sw:$,dev:x,buildId:k,buildExcludes:C,manifestEntries:B,manifestTransforms:P,modifyURLPrefix:y,publicPath:w.output?.publicPath}),X=resolveWorkboxPlugin({rootDir:j.dir,basePath:v,isDev:x,workboxCommon:Q,workboxOptions:G,importScripts:V$1,extendDefaultRuntimeCaching:q,dynamicStartUrl:O,hasFallbacks:U});w.plugins.push(X);}return w}}));exports.default=index;exports.runtimeCaching=t; +\ No newline at end of file ++const resolveRuntimeCaching=(o,n)=>{if(!o)return t;if(!n)return V.info("Custom runtimeCaching array found, using it instead of the default one."),o;V.info("Custom runtimeCaching array found, using it to extend the default one.");let a=[],i=new Set;for(let e of o)a.push(e),e.options?.cacheName&&i.add(e.options.cacheName);for(let e of t)e.options?.cacheName&&i.has(e.options.cacheName)||a.push(e);return a};const overrideAfterCalledMethod=e=>{Object.defineProperty(e,"alreadyCalled",{get:()=>!1,set(){}});};const isInjectManifestConfig=e=>void 0!==e&&"string"==typeof e.swSrc;const convertBoolean=(e,t=!0)=>{switch(typeof e){case"boolean":return e;case"number":case"bigint":return e>0;case"object":return null!==e;case"string":if(!t){if("false"===e||"0"===e)return !1;return !0}return "true"===e||"1"===e;case"function":case"symbol":return !0;case"undefined":return !1}};const getFileHash=r=>e$3.createHash("md5").update(e$2.readFileSync(r)).digest("hex");const getContentHash=(e,t)=>t?"development":getFileHash(e).slice(0,16);const resolveWorkboxPlugin=({rootDir:s,basePath:a,isDev:p,workboxCommon:l,workboxOptions:c,importScripts:u,extendDefaultRuntimeCaching:d,dynamicStartUrl:h,hasFallbacks:f})=>{if(isInjectManifestConfig(c)){let o=r.join(s,c.swSrc);V.info(`Using InjectManifest with ${o}`);let r$1=new t$1.InjectManifest({...l,...c,swSrc:o});return p&&overrideAfterCalledMethod(r$1),r$1}{let e;let{skipWaiting:r=!0,clientsClaim:s=!0,cleanupOutdatedCaches:m=!0,ignoreURLParametersMatching:g=[],importScripts:w,runtimeCaching:b}=c;w&&u.push(...w);let k=!1;p?(V.info("Building in development mode, caching and precaching are disabled for the most part. This means that offline support is disabled, but you can continue developing other functions in service worker."),g.push(/ts/),e=[{urlPattern:/.*/i,handler:"NetworkOnly",options:{cacheName:"dev"}}],k=!0):e=resolveRuntimeCaching(b,d),h&&e.unshift({urlPattern:a,handler:"NetworkFirst",options:{cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}}),f&&e.forEach(e=>{!e.options||e.options.precacheFallback||e.options.plugins?.find(e=>"handlerDidError"in e)||(e.options.plugins||(e.options.plugins=[]),e.options.plugins.push({handlerDidError:async({request:e})=>"undefined"!=typeof self?self.fallback(e):Response.error()}));});let y=new t$1.GenerateSW({...l,skipWaiting:r,clientsClaim:s,cleanupOutdatedCaches:m,ignoreURLParametersMatching:g,importScripts:u,...c,runtimeCaching:e});return k&&overrideAfterCalledMethod(y),y}};const defaultSwcRc={module:{type:"es6",lazy:!0,noInterop:!0},jsc:{parser:{syntax:"typescript",tsx:!0,dynamicImport:!0,decorators:!1},transform:{react:{runtime:"automatic"}},target:"es2022",loose:!1},minify:!1};let e=(t,e)=>{if(t)return e?.(t)};const NextPWAContext={shouldMinify:e(process.env.NEXT_PWA_MINIFY,convertBoolean),useSwcMinify:e(process.env.NEXT_PWA_SWC_MINIFY,convertBoolean)};const setDefaultContext=(t,e)=>{void 0===NextPWAContext[t]&&(NextPWAContext[t]=e);};let n=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)))),a=()=>({compress:{ecma:5,comparisons:!1,inline:2},mangle:{safari10:!0},format:{ecma:5,safari10:!0,comments:!1,ascii_only:!0},resolveSwc:J,useSwcMinify:NextPWAContext.useSwcMinify});const getSharedWebpackConfig=({swcRc:o=defaultSwcRc})=>{let i=NextPWAContext.shouldMinify&&{minimize:!0,minimizer:[new s({minify:q,terserOptions:a()})]};return {resolve:{extensions:[".js",".ts"],fallback:{module:!1,dgram:!1,dns:!1,path:!1,fs:!1,os:!1,crypto:!1,stream:!1,http2:!1,net:!1,tls:!1,zlib:!1,child_process:!1}},resolveLoader:{alias:{"swc-loader":r.join(n,"swc-loader.cjs")}},module:{rules:[{test:/\.(t|j)s$/i,use:[{loader:"swc-loader",options:o}]},{test:/\.m?js$/,resolve:{fullySpecified:false}}]},optimization:i||void 0}};const buildCustomWorker=({isDev:c$1,baseDir:a,customWorkerSrc:f,customWorkerDest:d,customWorkerPrefix:j,plugins:h=[],tsconfig:w,basePath:k})=>{let $=c([f,r.join("src",f)],t=>{t=r.join(a,t);let e=["ts","js"].map(o=>r.join(t,`index.${o}`)).filter(r=>e$2.existsSync(r));if(0===e.length)return;let n=e[0];return e.length>1&&V.info(`More than one custom worker found, ${n} will be used.`),n});if(!$)return;V.info(`Found a custom worker implementation at ${$}.`),w&&w.compilerOptions&&w.compilerOptions.paths&&a$1(defaultSwcRc,r.join(a,w.compilerOptions.baseUrl??"."),w.compilerOptions.paths);let b=`${j}-${getContentHash($,c$1)}.js`;return V.info(`Building custom worker to ${r.join(d,b)}...`),n$2({...getSharedWebpackConfig({swcRc:defaultSwcRc}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:$},output:{path:d,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(d,`${j}-*.js`),r.join(d,`${j}-*.js.map`)]}),...h]}).run((o,r)=>{(o||r?.hasErrors())&&(V.error("Failed to build custom worker."),V.error(r?.toString({colors:!0})),process.exit(-1));}),r.posix.join(k,b)};const getFallbackEnvs=({fallbacks:L,buildId:e})=>{let t=L.data;t&&t.endsWith(".json")&&(t=r.posix.join("/_next/data",e,t));let o={__PWA_FALLBACK_DOCUMENT__:L.document||!1,__PWA_FALLBACK_IMAGE__:L.image||!1,__PWA_FALLBACK_AUDIO__:L.audio||!1,__PWA_FALLBACK_VIDEO__:L.video||!1,__PWA_FALLBACK_FONT__:L.font||!1,__PWA_FALLBACK_DATA__:t||!1};if(0!==Object.values(o).filter(_=>!!_).length)return V.info("This app will fallback to these precached routes when fetching from the cache and the network fails:"),o.__PWA_FALLBACK_DOCUMENT__&&V.info(` Documents (pages): ${o.__PWA_FALLBACK_DOCUMENT__}`),o.__PWA_FALLBACK_IMAGE__&&V.info(` Images: ${o.__PWA_FALLBACK_IMAGE__}`),o.__PWA_FALLBACK_AUDIO__&&V.info(` Audio: ${o.__PWA_FALLBACK_AUDIO__}`),o.__PWA_FALLBACK_VIDEO__&&V.info(` Videos: ${o.__PWA_FALLBACK_VIDEO__}`),o.__PWA_FALLBACK_FONT__&&V.info(` Fonts: ${o.__PWA_FALLBACK_FONT__}`),o.__PWA_FALLBACK_DATA__&&V.info(` Data (/_next/data/**/*.json): ${o.__PWA_FALLBACK_DATA__}`),o};let m=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));const buildFallbackWorker=({isDev:e,buildId:c,fallbacks:p,destDir:u,basePath:f})=>{p=Object.keys(p).reduce((e,o)=>{let t=p[o];return t&&(e[o]=r.posix.join(f,t)),e},{});let j=getFallbackEnvs({fallbacks:p,buildId:c});if(!j)return;let k=r.join(m,"fallback.js"),b=`fallback-${getContentHash(k,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:k},output:{path:u,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"fallback-*.js"),r.join(u,"fallback-*.js.map")]}),new n$2.EnvironmentPlugin(j)]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build fallback worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),{name:r.posix.join(f,b),precaches:Object.values(j).filter(r=>!!r)}};let p=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));const buildSWEntryWorker=({isDev:e,destDir:u,shouldGenSWEWorker:l,basePath:a})=>{if(!l)return;let w=r.join(p,"sw-entry-worker.js"),c=`swe-worker-${getContentHash(w,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:w},output:{path:u,filename:c,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"swe-worker-*.js"),r.join(u,"swe-worker-*.js.map")]})]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build the service worker's sub-worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),r.posix.join(a,c)};const getDefaultDocumentPage=(t,f,n)=>{let s;let r$1=c(["pages","src/pages"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0));if(n&&(s=c(["app","src/app"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0))),r$1||s)for(let o of f){if(s){let t=r.join(s,`~offline/page.${o}`);if(e$2.existsSync(t))return "/~offline"}if(r$1){let t=r.join(r$1,`_offline.${o}`);if(t&&e$2.existsSync(t))return "/_offline"}}};let d=url.fileURLToPath(new URL(".",(typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));var index = ((e={})=>(t={})=>({...t,webpack(w,j){let h;try{h=require("next/dist/server/config-shared").defaultConfig;}catch{}let b=t.experimental?.appDir??h?.experimental?.appDir??!0,g=j.webpack,{buildId:k,dev:x,config:{distDir:y=".next",pageExtensions:A=["tsx","ts","jsx","js","mdx"]}}=j,v=j.config.basePath||"/",D=f(j.dir,t?.typescript?.tsconfigPath),{disable:P=!1,register:E=!0,dest:W=y,sw:$="sw.js",cacheStartUrl:S=!0,dynamicStartUrl:O=!0,dynamicStartUrlRedirect:R,publicExcludes:N=["!noprecache/**/*"],buildExcludes:C=[],fallbacks:L={},cacheOnFrontEndNav:M=!1,aggressiveFrontEndNavCaching:T=!1,reloadOnOnline:G=!0,scope:B=v,customWorkerDir:U,customWorkerSrc:F=U||"worker",customWorkerDest:H=W,customWorkerPrefix:I="worker",workboxOptions:Y={},extendDefaultRuntimeCaching:q=!1,swcMinify:K=t.swcMinify??h?.swcMinify??!1}=e;if("function"==typeof t.webpack&&(w=t.webpack(w,j)),P)return j.isServer&&V.info("PWA support is disabled."),w;let V$1=[];w.plugins||(w.plugins=[]),V.info(`Compiling for ${j.isServer?"server":"client (static)"}...`);let z=r.posix.join(v,$),J=r.posix.join(B,"/");w.plugins.push(new g.DefinePlugin({__PWA_SW__:`'${z}'`,__PWA_SCOPE__:`'${J}'`,__PWA_ENABLE_REGISTER__:`${!!E}`,__PWA_START_URL__:O?`'${v}'`:void 0,__PWA_CACHE_ON_FRONT_END_NAV__:`${!!M}`,__PWA_AGGRFEN_CACHE__:`${!!T}`,__PWA_RELOAD_ON_ONLINE__:`${!!G}`}));let Q=r.join(d,"sw-entry.js"),X=w.entry;if(w.entry=()=>X().then(i=>(i["main.js"]&&!i["main.js"].includes(Q)&&(Array.isArray(i["main.js"])?i["main.js"].unshift(Q):"string"==typeof i["main.js"]&&(i["main.js"]=[Q,i["main.js"]])),i["main-app"]&&!i["main-app"].includes(Q)&&(Array.isArray(i["main-app"])?i["main-app"].unshift(Q):"string"==typeof i["main-app"]&&(i["main-app"]=[Q,i["main-app"]])),i)),!j.isServer){setDefaultContext("shouldMinify",!x),setDefaultContext("useSwcMinify",K);let e=r.join(j.dir,W),o=r.join(j.dir,H),t=buildSWEntryWorker({isDev:x,destDir:e,shouldGenSWEWorker:M,basePath:v});w.plugins.push(new g.DefinePlugin({__PWA_SW_ENTRY_WORKER__:t&&`'${t}'`})),E||(V.info("Service worker won't be automatically registered as per the config, please call the following code in componentDidMount or useEffect:"),V.info(" window.workbox.register()"),D?.compilerOptions?.types?.includes("@ducanh2912/next-pwa/workbox")||V.info("You may also want to add @ducanh2912/next-pwa/workbox to compilerOptions.types in your tsconfig.json/jsconfig.json.")),V.info(`Service worker: ${r.join(e,$)}`),V.info(` URL: ${z}`),V.info(` Scope: ${J}`),w.plugins.push(new cleanWebpackPlugin.CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(e,"workbox-*.js"),r.join(e,"workbox-*.js.map"),r.join(e,$),r.join(e,`${$}.map`),r.join(e,"sw-chunks/**")]}));let d=buildCustomWorker({isDev:x,baseDir:j.dir,swDest:e,customWorkerSrc:F,customWorkerDest:o,customWorkerPrefix:I,plugins:w.plugins.filter(i=>i instanceof g.DefinePlugin),tsconfig:D,basePath:v});d&&V$1.unshift(d);let{additionalManifestEntries:h,modifyURLPrefix:y={},manifestTransforms:P=[],exclude:T,...G}=Y,B=h??[];B||(B=s$1.sync(["**/*","!workbox-*.js","!workbox-*.js.map","!worker-*.js","!worker-*.js.map","!fallback-*.js","!fallback-*.js.map",`!${$.replace(/^\/+/,"")}`,`!${$.replace(/^\/+/,"")}.map`,...N],{cwd:"public"}).map(e=>({url:r.posix.join(v,e),revision:getFileHash(`public/${e}`)}))),S&&(O?"string"==typeof R&&R.length>0&&B.push({url:R,revision:k}):B.push({url:v,revision:k})),Object.keys(G).forEach(i=>void 0===G[i]&&delete G[i]);let U=!1;if(L){L.document||(L.document=getDefaultDocumentPage(j.dir,A,b));let i=buildFallbackWorker({isDev:x,buildId:k,fallbacks:L,destDir:e,basePath:v});i&&(U=!0,V$1.unshift(i.name),i.precaches.forEach(i=>{i&&"boolean"!=typeof i&&!B.find(e=>"string"!=typeof e&&e.url.startsWith(i))&&B.push({url:i,revision:k});}));}let Q=resolveWorkboxCommon({dest:e,sw:$,dev:x,buildId:k,buildExcludes:C,manifestEntries:B,manifestTransforms:P,modifyURLPrefix:y,publicPath:w.output?.publicPath}),X=resolveWorkboxPlugin({rootDir:j.dir,basePath:v,isDev:x,workboxCommon:Q,workboxOptions:G,importScripts:V$1,extendDefaultRuntimeCaching:q,dynamicStartUrl:O,hasFallbacks:U});w.plugins.push(X);}return w}}));exports.default=index;exports.runtimeCaching=t; +\ No newline at end of file +diff --git a/node_modules/@ducanh2912/next-pwa/dist/index.module.js b/node_modules/@ducanh2912/next-pwa/dist/index.module.js +index 4389609..82ce509 100644 +--- a/node_modules/@ducanh2912/next-pwa/dist/index.module.js ++++ b/node_modules/@ducanh2912/next-pwa/dist/index.module.js +@@ -1,2 +1,2 @@ + import r from'path';import {fileURLToPath}from'url';import {createRequire}from'module';import e$2 from'fs';import n$1 from'process';import l from'os';import i from'tty';import {gte}from'semver';import {CleanWebpackPlugin}from'clean-webpack-plugin';import s$1 from'fast-glob';import t$1 from'workbox-webpack-plugin';import e$3 from'crypto';import n$2 from'webpack';import s from'terser-webpack-plugin';let e$1;let a$1=(e,r,t)=>{e.jsc||(e.jsc={}),e.jsc.baseUrl=r,e.jsc.paths=t;},c=(e,r)=>{for(let t of e){let e=r(t);if(e)return e}},u=createRequire(import.meta.url),p$1=e=>{try{return u(`${e}/package.json`).version}catch{return}},f=(e,r$1)=>{try{let n=c([r$1??"tsconfig.json","jsconfig.json"],r$1=>{let n=r.join(e,r$1);return e$2.existsSync(n)?n:void 0});if(!n)return;return JSON.parse(e$2.readFileSync(n,"utf-8"))}catch{return}},m$1=(e=0)=>r=>`\u001B[${r+e}m`,b=(e=0)=>r=>`\u001B[${38+e};5;${r}m`,g=(e=0)=>(r,t,o)=>`\u001B[${38+e};2;${r};${t};${o}m`,h={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};Object.keys(h.modifier);let d$1=Object.keys(h.color),O=Object.keys(h.bgColor);[...d$1,...O];let y=function(){let e=new Map;for(let[r,t]of Object.entries(h)){for(let[r,o]of Object.entries(t))h[r]={open:`\u001B[${o[0]}m`,close:`\u001B[${o[1]}m`},t[r]=h[r],e.set(o[0],o[1]);Object.defineProperty(h,r,{value:t,enumerable:!1});}return Object.defineProperty(h,"codes",{value:e,enumerable:!1}),h.color.close="\x1b[39m",h.bgColor.close="\x1b[49m",h.color.ansi=m$1(),h.color.ansi256=b(),h.color.ansi16m=g(),h.bgColor.ansi=m$1(10),h.bgColor.ansi256=b(10),h.bgColor.ansi16m=g(10),Object.defineProperties(h,{rgbToAnsi256:{value:(e,r,t)=>e===r&&r===t?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(r/255*5)+Math.round(t/255*5),enumerable:!1},hexToRgb:{value(e){let r=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!r)return [0,0,0];let[t]=r;3===t.length&&(t=[...t].map(e=>e+e).join(""));let o=Number.parseInt(t,16);return [o>>16&255,o>>8&255,255&o]},enumerable:!1},hexToAnsi256:{value:e=>h.rgbToAnsi256(...h.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){let r,t,o;if(e<8)return 30+e;if(e<16)return 90+(e-8);if(e>=232)t=r=((e-232)*10+8)/255,o=r;else {let n=(e-=16)%36;r=Math.floor(e/36)/5,t=Math.floor(n/6)/5,o=n%6/5;}let n=2*Math.max(r,t,o);if(0===n)return 30;let l=30+(Math.round(o)<<2|Math.round(t)<<1|Math.round(r));return 2===n&&(l+=60),l},enumerable:!1},rgbToAnsi:{value:(e,r,t)=>h.ansi256ToAnsi(h.rgbToAnsi256(e,r,t)),enumerable:!1},hexToAnsi:{value:e=>h.ansi256ToAnsi(h.hexToAnsi256(e)),enumerable:!1}}),h}();function v(e,r=globalThis.Deno?globalThis.Deno.args:n$1.argv){let t=e.startsWith("-")?"":1===e.length?"-":"--",o=r.indexOf(t+e),l=r.indexOf("--");return -1!==o&&(-1===l||o=10&&Number(e[2])>=10586?Number(e[2])>=14931?3:2:1}if("CI"in T)return "GITHUB_ACTIONS"in T||"GITEA_ACTIONS"in T?3:["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(e=>e in T)||"codeship"===T.CI_NAME?1:a;if("TEAMCITY_VERSION"in T)return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(T.TEAMCITY_VERSION)?1:0;if("truecolor"===T.COLORTERM||"xterm-kitty"===T.TERM)return 3;if("TERM_PROGRAM"in T){let e=Number.parseInt((T.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(T.TERM_PROGRAM){case"iTerm.app":return e>=3?3:2;case"Apple_Terminal":return 2}}return /-256(color)?$/i.test(T.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(T.TERM)||"COLORTERM"in T?1:a}(r,{streamIsTTY:r&&r.isTTY,...t});return 0!==o&&{level:o,hasBasic:!0,has256:o>=2,has16m:o>=3}}v("no-color")||v("no-colors")||v("color=false")||v("color=never")?e$1=0:(v("color")||v("colors")||v("color=true")||v("color=always"))&&(e$1=1);let{stdout:j,stderr:R}={stdout:M({isTTY:i.isatty(1)}),stderr:M({isTTY:i.isatty(2)})},w=Symbol("GENERATOR"),C=Symbol("STYLER"),A=Symbol("IS_EMPTY"),E=["ansi","ansi","ansi256","ansi16m"],B=Object.create(null),S=(e,r={})=>{if(r.level&&!(Number.isInteger(r.level)&&r.level>=0&&r.level<=3))throw Error("The `level` option should be an integer from 0 to 3");let t=j?j.level:0;e.level=void 0===r.level?t:r.level;},x=e=>{let r=(...e)=>e.join(" ");return S(r,e),Object.setPrototypeOf(r,I.prototype),r};function I(e){return x(e)}for(let[e,r]of(Object.setPrototypeOf(I.prototype,Function.prototype),Object.entries(y)))B[e]={get(){let t=$(this,_(r.open,r.close,this[C]),this[A]);return Object.defineProperty(this,e,{value:t}),t}};B.visible={get(){let e=$(this,this[C],!0);return Object.defineProperty(this,"visible",{value:e}),e}};let P=(e,r,t,...o)=>"rgb"===e?"ansi16m"===r?y[t].ansi16m(...o):"ansi256"===r?y[t].ansi256(y.rgbToAnsi256(...o)):y[t].ansi(y.rgbToAnsi(...o)):"hex"===e?P("rgb",r,t,...y.hexToRgb(...o)):y[t][e](...o);for(let e of ["rgb","hex","ansi256"])B[e]={get(){let{level:r}=this;return function(...t){return $(this,_(P(e,E[r],"color",...t),y.color.close,this[C]),this[A])}}},B["bg"+e[0].toUpperCase()+e.slice(1)]={get(){let{level:r}=this;return function(...t){return $(this,_(P(e,E[r],"bgColor",...t),y.bgColor.close,this[C]),this[A])}}};let N=Object.defineProperties(()=>{},{...B,level:{enumerable:!0,get(){return this[w].level},set(e){this[w].level=e;}}}),_=(e,r,t)=>{let o,n;return void 0===t?(o=e,n=r):(o=t.openAll+e,n=r+t.closeAll),{open:e,close:r,openAll:o,closeAll:n,parent:t}},$=(e,r,t)=>{let o=(...e)=>k(o,1===e.length?""+e[0]:e.join(" "));return Object.setPrototypeOf(o,N),o[w]=e,o[C]=r,o[A]=t,o},k=(e,r)=>{if(e.level<=0||!r)return e[A]?"":r;let t=e[C];if(void 0===t)return r;let{openAll:o,closeAll:n}=t;if(r.includes("\x1b"))for(;void 0!==t;)r=function(e,r,t){let o=e.indexOf(r);if(-1===o)return e;let n=r.length,l=0,i="";do i+=e.slice(l,o)+r+t,l=o+n,o=e.indexOf(r,l);while(-1!==o)return i+e.slice(l)}(r,t.close,t.open),t=t.parent;let l=r.indexOf("\n");return -1!==l&&(r=function(e,r,t,o){let n=0,l="";do{let i="\r"===e[o-1];l+=e.slice(n,i?o-1:o)+r+(i?"\r\n":"\n")+t,n=o+1,o=e.indexOf("\n",n);}while(-1!==o)return l+e.slice(n)}(r,n,o,l)),o+r+n};Object.defineProperties(I.prototype,B);let L=x(void 0);x({level:R?R.level:0});let F=p$1("next"),G=!!F&>e(F,"13.4.1"),Y=(e,r=0)=>G?`- ${e} (pwa)`:`${e}${" ".repeat(r)}- (PWA)`,D={wait:Y(L.cyan("wait"),2),error:Y(L.red("error"),1),warn:Y(L.yellow("warn"),2),info:Y(L.cyan("info"),2)};var V=Object.freeze({__proto__:null,error:(...e)=>{console.error(D.error,...e);},info:(...e)=>{console.log(D.info,...e);},prefixes:D,wait:(...e)=>{console.log(D.wait,...e);},warn:(...e)=>{console.warn(D.warn,...e);}});let J=()=>{let e;for(let r of ["@swc/core","next/dist/build/swc"])try{e=require(r);break}catch{}if(!e)throw Error("Failed to resolve swc. Please install @swc/core if you haven't.");return e};createRequire(import.meta.url);let q=async(e,r,t,o)=>{let{resolveSwc:n,useSwcMinify:l,...i}=t,s=()=>require("terser-webpack-plugin").terserMinify(e,r,i,o);if(l){let t,o;try{t=n();}catch{return s()}if(!t.minify)return s();let l={...i,compress:"boolean"==typeof i.compress?!!i.compress&&{}:{...i.compress},mangle:null==i.mangle||("boolean"==typeof i.mangle?i.mangle:{...i.mangle}),sourceMap:void 0};r&&(l.sourceMap=!0),l.compress&&(void 0===l.compress.ecma&&(l.compress.ecma=l.ecma),5===l.ecma&&void 0===l.compress.arrows&&(l.compress.arrows=!1));let[[a,c]]=Object.entries(e),u=await t.minify(c,l);return u.map&&((o=JSON.parse(u.map)).sources=[a],delete o.sourcesContent),{code:u.code,map:o}}return s()};var t = [{urlPattern:/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,handler:"CacheFirst",options:{cacheName:"google-fonts-webfonts",expiration:{maxEntries:4,maxAgeSeconds:31536e3}}},{urlPattern:/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,handler:"StaleWhileRevalidate",options:{cacheName:"google-fonts-stylesheets",expiration:{maxEntries:4,maxAgeSeconds:604800}}},{urlPattern:/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-font-assets",expiration:{maxEntries:4,maxAgeSeconds:604800}}},{urlPattern:/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-image-assets",expiration:{maxEntries:64,maxAgeSeconds:2592e3}}},{urlPattern:/\/_next\/static.+\.js$/i,handler:"CacheFirst",options:{cacheName:"next-static-js-assets",expiration:{maxEntries:64,maxAgeSeconds:86400}}},{urlPattern:/\/_next\/image\?url=.+$/i,handler:"StaleWhileRevalidate",options:{cacheName:"next-image",expiration:{maxEntries:64,maxAgeSeconds:86400}}},{urlPattern:/\.(?:mp3|wav|ogg)$/i,handler:"CacheFirst",options:{rangeRequests:!0,cacheName:"static-audio-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:mp4)$/i,handler:"CacheFirst",options:{rangeRequests:!0,cacheName:"static-video-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:js)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-js-assets",expiration:{maxEntries:48,maxAgeSeconds:86400}}},{urlPattern:/\.(?:css|less)$/i,handler:"StaleWhileRevalidate",options:{cacheName:"static-style-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\/_next\/data\/.+\/.+\.json$/i,handler:"StaleWhileRevalidate",options:{cacheName:"next-data",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:/\.(?:json|xml|csv)$/i,handler:"NetworkFirst",options:{cacheName:"static-data-assets",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({sameOrigin:e,url:{pathname:t}})=>!(!e||t.startsWith("/api/auth/"))&&!!t.startsWith("/api/"),handler:"NetworkFirst",method:"GET",options:{cacheName:"apis",expiration:{maxEntries:16,maxAgeSeconds:86400},networkTimeoutSeconds:10}},{urlPattern:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!t.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages-rsc-prefetch",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!t.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages-rsc",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({url:{pathname:e},sameOrigin:t})=>t&&!e.startsWith("/api/"),handler:"NetworkFirst",options:{cacheName:"pages",expiration:{maxEntries:32,maxAgeSeconds:86400}}},{urlPattern:({sameOrigin:e})=>!e,handler:"NetworkFirst",options:{cacheName:"cross-origin",expiration:{maxEntries:32,maxAgeSeconds:3600},networkTimeoutSeconds:10}}];const resolveWorkboxCommon=({dest:e,sw:a,dev:r$1,buildId:n,buildExcludes:s,manifestEntries:i,manifestTransforms:l,modifyURLPrefix:o,publicPath:m})=>({swDest:r.join(e,a),additionalManifestEntries:r$1?[]:i,exclude:[...s,({asset:t})=>!!(t.name.startsWith("server/")||t.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/))||!!r$1&&!t.name.startsWith("static/runtime/")],modifyURLPrefix:{...o,"/_next/../public/":"/"},manifestTransforms:[...l,async(t,e)=>{let a=t.map(t=>{if(t.url=t.url.replace("/_next//static/image","/_next/static/image"),t.url=t.url.replace("/_next//static/media","/_next/static/media"),null===t.revision){let a=t.url;"string"==typeof m&&a.startsWith(m)&&(a=t.url.substring(m.length));let r=e.assetsInfo.get(a);t.revision=r&&r.contenthash||n;}return t.url=t.url.replace(/\[/g,"%5B").replace(/\]/g,"%5D"),t});return {manifest:a,warnings:[]}}]}); +-const resolveRuntimeCaching=(o,n)=>{if(!o)return t;if(!n)return V.info("Custom runtimeCaching array found, using it instead of the default one."),o;V.info("Custom runtimeCaching array found, using it to extend the default one.");let a=[],i=new Set;for(let e of o)a.push(e),e.options?.cacheName&&i.add(e.options.cacheName);for(let e of t)e.options?.cacheName&&i.has(e.options.cacheName)||a.push(e);return a};const overrideAfterCalledMethod=e=>{Object.defineProperty(e,"alreadyCalled",{get:()=>!1,set(){}});};const isInjectManifestConfig=e=>void 0!==e&&"string"==typeof e.swSrc;const convertBoolean=(e,t=!0)=>{switch(typeof e){case"boolean":return e;case"number":case"bigint":return e>0;case"object":return null!==e;case"string":if(!t){if("false"===e||"0"===e)return !1;return !0}return "true"===e||"1"===e;case"function":case"symbol":return !0;case"undefined":return !1}};const getFileHash=r=>e$3.createHash("md5").update(e$2.readFileSync(r)).digest("hex");const getContentHash=(e,t)=>t?"development":getFileHash(e).slice(0,16);const resolveWorkboxPlugin=({rootDir:s,basePath:a,isDev:p,workboxCommon:l,workboxOptions:c,importScripts:u,extendDefaultRuntimeCaching:d,dynamicStartUrl:h,hasFallbacks:f})=>{if(isInjectManifestConfig(c)){let o=r.join(s,c.swSrc);V.info(`Using InjectManifest with ${o}`);let r$1=new t$1.InjectManifest({...l,...c,swSrc:o});return p&&overrideAfterCalledMethod(r$1),r$1}{let e;let{skipWaiting:r=!0,clientsClaim:s=!0,cleanupOutdatedCaches:m=!0,ignoreURLParametersMatching:g=[],importScripts:w,runtimeCaching:b}=c;w&&u.push(...w);let k=!1;p?(V.info("Building in development mode, caching and precaching are disabled for the most part. This means that offline support is disabled, but you can continue developing other functions in service worker."),g.push(/ts/),e=[{urlPattern:/.*/i,handler:"NetworkOnly",options:{cacheName:"dev"}}],k=!0):e=resolveRuntimeCaching(b,d),h&&e.unshift({urlPattern:a,handler:"NetworkFirst",options:{cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}}),f&&e.forEach(e=>{!e.options||e.options.precacheFallback||e.options.plugins?.find(e=>"handlerDidError"in e)||(e.options.plugins||(e.options.plugins=[]),e.options.plugins.push({handlerDidError:async({request:e})=>"undefined"!=typeof self?self.fallback(e):Response.error()}));});let y=new t$1.GenerateSW({...l,skipWaiting:r,clientsClaim:s,cleanupOutdatedCaches:m,ignoreURLParametersMatching:g,importScripts:u,...c,runtimeCaching:e});return k&&overrideAfterCalledMethod(y),y}};const defaultSwcRc={module:{type:"es6",lazy:!0,noInterop:!0},jsc:{parser:{syntax:"typescript",tsx:!0,dynamicImport:!0,decorators:!1},transform:{react:{runtime:"automatic"}},target:"es2022",loose:!1},minify:!1};let e=(t,e)=>{if(t)return e?.(t)};const NextPWAContext={shouldMinify:e(process.env.NEXT_PWA_MINIFY,convertBoolean),useSwcMinify:e(process.env.NEXT_PWA_SWC_MINIFY,convertBoolean)};const setDefaultContext=(t,e)=>{void 0===NextPWAContext[t]&&(NextPWAContext[t]=e);};let n=fileURLToPath(new URL(".",import.meta.url)),a=()=>({compress:{ecma:5,comparisons:!1,inline:2},mangle:{safari10:!0},format:{ecma:5,safari10:!0,comments:!1,ascii_only:!0},resolveSwc:J,useSwcMinify:NextPWAContext.useSwcMinify});const getSharedWebpackConfig=({swcRc:o=defaultSwcRc})=>{let i=NextPWAContext.shouldMinify&&{minimize:!0,minimizer:[new s({minify:q,terserOptions:a()})]};return {resolve:{extensions:[".js",".ts"],fallback:{module:!1,dgram:!1,dns:!1,path:!1,fs:!1,os:!1,crypto:!1,stream:!1,http2:!1,net:!1,tls:!1,zlib:!1,child_process:!1}},resolveLoader:{alias:{"swc-loader":r.join(n,"swc-loader.cjs")}},module:{rules:[{test:/\.(t|j)s$/i,use:[{loader:"swc-loader",options:o}]}]},optimization:i||void 0}};const buildCustomWorker=({isDev:c$1,baseDir:a,customWorkerSrc:f,customWorkerDest:d,customWorkerPrefix:j,plugins:h=[],tsconfig:w,basePath:k})=>{let $=c([f,r.join("src",f)],t=>{t=r.join(a,t);let e=["ts","js"].map(o=>r.join(t,`index.${o}`)).filter(r=>e$2.existsSync(r));if(0===e.length)return;let n=e[0];return e.length>1&&V.info(`More than one custom worker found, ${n} will be used.`),n});if(!$)return;V.info(`Found a custom worker implementation at ${$}.`),w&&w.compilerOptions&&w.compilerOptions.paths&&a$1(defaultSwcRc,r.join(a,w.compilerOptions.baseUrl??"."),w.compilerOptions.paths);let b=`${j}-${getContentHash($,c$1)}.js`;return V.info(`Building custom worker to ${r.join(d,b)}...`),n$2({...getSharedWebpackConfig({swcRc:defaultSwcRc}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:$},output:{path:d,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(d,`${j}-*.js`),r.join(d,`${j}-*.js.map`)]}),...h]}).run((o,r)=>{(o||r?.hasErrors())&&(V.error("Failed to build custom worker."),V.error(r?.toString({colors:!0})),process.exit(-1));}),r.posix.join(k,b)};const getFallbackEnvs=({fallbacks:L,buildId:e})=>{let t=L.data;t&&t.endsWith(".json")&&(t=r.posix.join("/_next/data",e,t));let o={__PWA_FALLBACK_DOCUMENT__:L.document||!1,__PWA_FALLBACK_IMAGE__:L.image||!1,__PWA_FALLBACK_AUDIO__:L.audio||!1,__PWA_FALLBACK_VIDEO__:L.video||!1,__PWA_FALLBACK_FONT__:L.font||!1,__PWA_FALLBACK_DATA__:t||!1};if(0!==Object.values(o).filter(_=>!!_).length)return V.info("This app will fallback to these precached routes when fetching from the cache and the network fails:"),o.__PWA_FALLBACK_DOCUMENT__&&V.info(` Documents (pages): ${o.__PWA_FALLBACK_DOCUMENT__}`),o.__PWA_FALLBACK_IMAGE__&&V.info(` Images: ${o.__PWA_FALLBACK_IMAGE__}`),o.__PWA_FALLBACK_AUDIO__&&V.info(` Audio: ${o.__PWA_FALLBACK_AUDIO__}`),o.__PWA_FALLBACK_VIDEO__&&V.info(` Videos: ${o.__PWA_FALLBACK_VIDEO__}`),o.__PWA_FALLBACK_FONT__&&V.info(` Fonts: ${o.__PWA_FALLBACK_FONT__}`),o.__PWA_FALLBACK_DATA__&&V.info(` Data (/_next/data/**/*.json): ${o.__PWA_FALLBACK_DATA__}`),o};let m=fileURLToPath(new URL(".",import.meta.url));const buildFallbackWorker=({isDev:e,buildId:c,fallbacks:p,destDir:u,basePath:f})=>{p=Object.keys(p).reduce((e,o)=>{let t=p[o];return t&&(e[o]=r.posix.join(f,t)),e},{});let j=getFallbackEnvs({fallbacks:p,buildId:c});if(!j)return;let k=r.join(m,"fallback.js"),b=`fallback-${getContentHash(k,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:k},output:{path:u,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"fallback-*.js"),r.join(u,"fallback-*.js.map")]}),new n$2.EnvironmentPlugin(j)]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build fallback worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),{name:r.posix.join(f,b),precaches:Object.values(j).filter(r=>!!r)}};let p=fileURLToPath(new URL(".",import.meta.url));const buildSWEntryWorker=({isDev:e,destDir:u,shouldGenSWEWorker:l,basePath:a})=>{if(!l)return;let w=r.join(p,"sw-entry-worker.js"),c=`swe-worker-${getContentHash(w,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:w},output:{path:u,filename:c,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"swe-worker-*.js"),r.join(u,"swe-worker-*.js.map")]})]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build the service worker's sub-worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),r.posix.join(a,c)};const getDefaultDocumentPage=(t,f,n)=>{let s;let r$1=c(["pages","src/pages"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0));if(n&&(s=c(["app","src/app"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0))),r$1||s)for(let o of f){if(s){let t=r.join(s,`~offline/page.${o}`);if(e$2.existsSync(t))return "/~offline"}if(r$1){let t=r.join(r$1,`_offline.${o}`);if(t&&e$2.existsSync(t))return "/_offline"}}};let d=fileURLToPath(new URL(".",import.meta.url));var index = ((e={})=>(t={})=>({...t,webpack(w,j){let h;try{h=require("next/dist/server/config-shared").defaultConfig;}catch{}let b=t.experimental?.appDir??h?.experimental?.appDir??!0,g=j.webpack,{buildId:k,dev:x,config:{distDir:y=".next",pageExtensions:A=["tsx","ts","jsx","js","mdx"]}}=j,v=j.config.basePath||"/",D=f(j.dir,t?.typescript?.tsconfigPath),{disable:P=!1,register:E=!0,dest:W=y,sw:$="sw.js",cacheStartUrl:S=!0,dynamicStartUrl:O=!0,dynamicStartUrlRedirect:R,publicExcludes:N=["!noprecache/**/*"],buildExcludes:C=[],fallbacks:L={},cacheOnFrontEndNav:M=!1,aggressiveFrontEndNavCaching:T=!1,reloadOnOnline:G=!0,scope:B=v,customWorkerDir:U,customWorkerSrc:F=U||"worker",customWorkerDest:H=W,customWorkerPrefix:I="worker",workboxOptions:Y={},extendDefaultRuntimeCaching:q=!1,swcMinify:K=t.swcMinify??h?.swcMinify??!1}=e;if("function"==typeof t.webpack&&(w=t.webpack(w,j)),P)return j.isServer&&V.info("PWA support is disabled."),w;let V$1=[];w.plugins||(w.plugins=[]),V.info(`Compiling for ${j.isServer?"server":"client (static)"}...`);let z=r.posix.join(v,$),J=r.posix.join(B,"/");w.plugins.push(new g.DefinePlugin({__PWA_SW__:`'${z}'`,__PWA_SCOPE__:`'${J}'`,__PWA_ENABLE_REGISTER__:`${!!E}`,__PWA_START_URL__:O?`'${v}'`:void 0,__PWA_CACHE_ON_FRONT_END_NAV__:`${!!M}`,__PWA_AGGRFEN_CACHE__:`${!!T}`,__PWA_RELOAD_ON_ONLINE__:`${!!G}`}));let Q=r.join(d,"sw-entry.js"),X=w.entry;if(w.entry=()=>X().then(i=>(i["main.js"]&&!i["main.js"].includes(Q)&&(Array.isArray(i["main.js"])?i["main.js"].unshift(Q):"string"==typeof i["main.js"]&&(i["main.js"]=[Q,i["main.js"]])),i["main-app"]&&!i["main-app"].includes(Q)&&(Array.isArray(i["main-app"])?i["main-app"].unshift(Q):"string"==typeof i["main-app"]&&(i["main-app"]=[Q,i["main-app"]])),i)),!j.isServer){setDefaultContext("shouldMinify",!x),setDefaultContext("useSwcMinify",K);let e=r.join(j.dir,W),o=r.join(j.dir,H),t=buildSWEntryWorker({isDev:x,destDir:e,shouldGenSWEWorker:M,basePath:v});w.plugins.push(new g.DefinePlugin({__PWA_SW_ENTRY_WORKER__:t&&`'${t}'`})),E||(V.info("Service worker won't be automatically registered as per the config, please call the following code in componentDidMount or useEffect:"),V.info(" window.workbox.register()"),D?.compilerOptions?.types?.includes("@ducanh2912/next-pwa/workbox")||V.info("You may also want to add @ducanh2912/next-pwa/workbox to compilerOptions.types in your tsconfig.json/jsconfig.json.")),V.info(`Service worker: ${r.join(e,$)}`),V.info(` URL: ${z}`),V.info(` Scope: ${J}`),w.plugins.push(new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(e,"workbox-*.js"),r.join(e,"workbox-*.js.map"),r.join(e,$),r.join(e,`${$}.map`),r.join(e,"sw-chunks/**")]}));let d=buildCustomWorker({isDev:x,baseDir:j.dir,swDest:e,customWorkerSrc:F,customWorkerDest:o,customWorkerPrefix:I,plugins:w.plugins.filter(i=>i instanceof g.DefinePlugin),tsconfig:D,basePath:v});d&&V$1.unshift(d);let{additionalManifestEntries:h,modifyURLPrefix:y={},manifestTransforms:P=[],exclude:T,...G}=Y,B=h??[];B||(B=s$1.sync(["**/*","!workbox-*.js","!workbox-*.js.map","!worker-*.js","!worker-*.js.map","!fallback-*.js","!fallback-*.js.map",`!${$.replace(/^\/+/,"")}`,`!${$.replace(/^\/+/,"")}.map`,...N],{cwd:"public"}).map(e=>({url:r.posix.join(v,e),revision:getFileHash(`public/${e}`)}))),S&&(O?"string"==typeof R&&R.length>0&&B.push({url:R,revision:k}):B.push({url:v,revision:k})),Object.keys(G).forEach(i=>void 0===G[i]&&delete G[i]);let U=!1;if(L){L.document||(L.document=getDefaultDocumentPage(j.dir,A,b));let i=buildFallbackWorker({isDev:x,buildId:k,fallbacks:L,destDir:e,basePath:v});i&&(U=!0,V$1.unshift(i.name),i.precaches.forEach(i=>{i&&"boolean"!=typeof i&&!B.find(e=>"string"!=typeof e&&e.url.startsWith(i))&&B.push({url:i,revision:k});}));}let Q=resolveWorkboxCommon({dest:e,sw:$,dev:x,buildId:k,buildExcludes:C,manifestEntries:B,manifestTransforms:P,modifyURLPrefix:y,publicPath:w.output?.publicPath}),X=resolveWorkboxPlugin({rootDir:j.dir,basePath:v,isDev:x,workboxCommon:Q,workboxOptions:G,importScripts:V$1,extendDefaultRuntimeCaching:q,dynamicStartUrl:O,hasFallbacks:U});w.plugins.push(X);}return w}}));export{index as default,t as runtimeCaching}; +\ No newline at end of file ++const resolveRuntimeCaching=(o,n)=>{if(!o)return t;if(!n)return V.info("Custom runtimeCaching array found, using it instead of the default one."),o;V.info("Custom runtimeCaching array found, using it to extend the default one.");let a=[],i=new Set;for(let e of o)a.push(e),e.options?.cacheName&&i.add(e.options.cacheName);for(let e of t)e.options?.cacheName&&i.has(e.options.cacheName)||a.push(e);return a};const overrideAfterCalledMethod=e=>{Object.defineProperty(e,"alreadyCalled",{get:()=>!1,set(){}});};const isInjectManifestConfig=e=>void 0!==e&&"string"==typeof e.swSrc;const convertBoolean=(e,t=!0)=>{switch(typeof e){case"boolean":return e;case"number":case"bigint":return e>0;case"object":return null!==e;case"string":if(!t){if("false"===e||"0"===e)return !1;return !0}return "true"===e||"1"===e;case"function":case"symbol":return !0;case"undefined":return !1}};const getFileHash=r=>e$3.createHash("md5").update(e$2.readFileSync(r)).digest("hex");const getContentHash=(e,t)=>t?"development":getFileHash(e).slice(0,16);const resolveWorkboxPlugin=({rootDir:s,basePath:a,isDev:p,workboxCommon:l,workboxOptions:c,importScripts:u,extendDefaultRuntimeCaching:d,dynamicStartUrl:h,hasFallbacks:f})=>{if(isInjectManifestConfig(c)){let o=r.join(s,c.swSrc);V.info(`Using InjectManifest with ${o}`);let r$1=new t$1.InjectManifest({...l,...c,swSrc:o});return p&&overrideAfterCalledMethod(r$1),r$1}{let e;let{skipWaiting:r=!0,clientsClaim:s=!0,cleanupOutdatedCaches:m=!0,ignoreURLParametersMatching:g=[],importScripts:w,runtimeCaching:b}=c;w&&u.push(...w);let k=!1;p?(V.info("Building in development mode, caching and precaching are disabled for the most part. This means that offline support is disabled, but you can continue developing other functions in service worker."),g.push(/ts/),e=[{urlPattern:/.*/i,handler:"NetworkOnly",options:{cacheName:"dev"}}],k=!0):e=resolveRuntimeCaching(b,d),h&&e.unshift({urlPattern:a,handler:"NetworkFirst",options:{cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}}),f&&e.forEach(e=>{!e.options||e.options.precacheFallback||e.options.plugins?.find(e=>"handlerDidError"in e)||(e.options.plugins||(e.options.plugins=[]),e.options.plugins.push({handlerDidError:async({request:e})=>"undefined"!=typeof self?self.fallback(e):Response.error()}));});let y=new t$1.GenerateSW({...l,skipWaiting:r,clientsClaim:s,cleanupOutdatedCaches:m,ignoreURLParametersMatching:g,importScripts:u,...c,runtimeCaching:e});return k&&overrideAfterCalledMethod(y),y}};const defaultSwcRc={module:{type:"es6",lazy:!0,noInterop:!0},jsc:{parser:{syntax:"typescript",tsx:!0,dynamicImport:!0,decorators:!1},transform:{react:{runtime:"automatic"}},target:"es2022",loose:!1},minify:!1};let e=(t,e)=>{if(t)return e?.(t)};const NextPWAContext={shouldMinify:e(process.env.NEXT_PWA_MINIFY,convertBoolean),useSwcMinify:e(process.env.NEXT_PWA_SWC_MINIFY,convertBoolean)};const setDefaultContext=(t,e)=>{void 0===NextPWAContext[t]&&(NextPWAContext[t]=e);};let n=fileURLToPath(new URL(".",import.meta.url)),a=()=>({compress:{ecma:5,comparisons:!1,inline:2},mangle:{safari10:!0},format:{ecma:5,safari10:!0,comments:!1,ascii_only:!0},resolveSwc:J,useSwcMinify:NextPWAContext.useSwcMinify});const getSharedWebpackConfig=({swcRc:o=defaultSwcRc})=>{let i=NextPWAContext.shouldMinify&&{minimize:!0,minimizer:[new s({minify:q,terserOptions:a()})]};return {resolve:{extensions:[".js",".ts"],fallback:{module:!1,dgram:!1,dns:!1,path:!1,fs:!1,os:!1,crypto:!1,stream:!1,http2:!1,net:!1,tls:!1,zlib:!1,child_process:!1}},resolveLoader:{alias:{"swc-loader":r.join(n,"swc-loader.cjs")}},module:{rules:[{test:/\.(t|j)s$/i,use:[{loader:"swc-loader",options:o}]},{test:/\.m?js$/,resolve:{fullySpecified:false}}]},optimization:i||void 0}};const buildCustomWorker=({isDev:c$1,baseDir:a,customWorkerSrc:f,customWorkerDest:d,customWorkerPrefix:j,plugins:h=[],tsconfig:w,basePath:k})=>{let $=c([f,r.join("src",f)],t=>{t=r.join(a,t);let e=["ts","js"].map(o=>r.join(t,`index.${o}`)).filter(r=>e$2.existsSync(r));if(0===e.length)return;let n=e[0];return e.length>1&&V.info(`More than one custom worker found, ${n} will be used.`),n});if(!$)return;V.info(`Found a custom worker implementation at ${$}.`),w&&w.compilerOptions&&w.compilerOptions.paths&&a$1(defaultSwcRc,r.join(a,w.compilerOptions.baseUrl??"."),w.compilerOptions.paths);let b=`${j}-${getContentHash($,c$1)}.js`;return V.info(`Building custom worker to ${r.join(d,b)}...`),n$2({...getSharedWebpackConfig({swcRc:defaultSwcRc}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:$},output:{path:d,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(d,`${j}-*.js`),r.join(d,`${j}-*.js.map`)]}),...h]}).run((o,r)=>{(o||r?.hasErrors())&&(V.error("Failed to build custom worker."),V.error(r?.toString({colors:!0})),process.exit(-1));}),r.posix.join(k,b)};const getFallbackEnvs=({fallbacks:L,buildId:e})=>{let t=L.data;t&&t.endsWith(".json")&&(t=r.posix.join("/_next/data",e,t));let o={__PWA_FALLBACK_DOCUMENT__:L.document||!1,__PWA_FALLBACK_IMAGE__:L.image||!1,__PWA_FALLBACK_AUDIO__:L.audio||!1,__PWA_FALLBACK_VIDEO__:L.video||!1,__PWA_FALLBACK_FONT__:L.font||!1,__PWA_FALLBACK_DATA__:t||!1};if(0!==Object.values(o).filter(_=>!!_).length)return V.info("This app will fallback to these precached routes when fetching from the cache and the network fails:"),o.__PWA_FALLBACK_DOCUMENT__&&V.info(` Documents (pages): ${o.__PWA_FALLBACK_DOCUMENT__}`),o.__PWA_FALLBACK_IMAGE__&&V.info(` Images: ${o.__PWA_FALLBACK_IMAGE__}`),o.__PWA_FALLBACK_AUDIO__&&V.info(` Audio: ${o.__PWA_FALLBACK_AUDIO__}`),o.__PWA_FALLBACK_VIDEO__&&V.info(` Videos: ${o.__PWA_FALLBACK_VIDEO__}`),o.__PWA_FALLBACK_FONT__&&V.info(` Fonts: ${o.__PWA_FALLBACK_FONT__}`),o.__PWA_FALLBACK_DATA__&&V.info(` Data (/_next/data/**/*.json): ${o.__PWA_FALLBACK_DATA__}`),o};let m=fileURLToPath(new URL(".",import.meta.url));const buildFallbackWorker=({isDev:e,buildId:c,fallbacks:p,destDir:u,basePath:f})=>{p=Object.keys(p).reduce((e,o)=>{let t=p[o];return t&&(e[o]=r.posix.join(f,t)),e},{});let j=getFallbackEnvs({fallbacks:p,buildId:c});if(!j)return;let k=r.join(m,"fallback.js"),b=`fallback-${getContentHash(k,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:k},output:{path:u,filename:b,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"fallback-*.js"),r.join(u,"fallback-*.js.map")]}),new n$2.EnvironmentPlugin(j)]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build fallback worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),{name:r.posix.join(f,b),precaches:Object.values(j).filter(r=>!!r)}};let p=fileURLToPath(new URL(".",import.meta.url));const buildSWEntryWorker=({isDev:e,destDir:u,shouldGenSWEWorker:l,basePath:a})=>{if(!l)return;let w=r.join(p,"sw-entry-worker.js"),c=`swe-worker-${getContentHash(w,e)}.js`;return n$2({...getSharedWebpackConfig({}),mode:NextPWAContext.shouldMinify?"production":"development",target:"webworker",entry:{main:w},output:{path:u,filename:c,chunkFilename:"sw-chunks/[id]-[chunkhash].js"},plugins:[new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(u,"swe-worker-*.js"),r.join(u,"swe-worker-*.js.map")]})]}).run((r,e)=>{(r||e?.hasErrors())&&(V.error("Failed to build the service worker's sub-worker."),V.error(e?.toString({colors:!0})),process.exit(-1));}),r.posix.join(a,c)};const getDefaultDocumentPage=(t,f,n)=>{let s;let r$1=c(["pages","src/pages"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0));if(n&&(s=c(["app","src/app"],o=>(o=r.join(t,o),e$2.existsSync(o)?o:void 0))),r$1||s)for(let o of f){if(s){let t=r.join(s,`~offline/page.${o}`);if(e$2.existsSync(t))return "/~offline"}if(r$1){let t=r.join(r$1,`_offline.${o}`);if(t&&e$2.existsSync(t))return "/_offline"}}};let d=fileURLToPath(new URL(".",import.meta.url));var index = ((e={})=>(t={})=>({...t,webpack(w,j){let h;try{h=require("next/dist/server/config-shared").defaultConfig;}catch{}let b=t.experimental?.appDir??h?.experimental?.appDir??!0,g=j.webpack,{buildId:k,dev:x,config:{distDir:y=".next",pageExtensions:A=["tsx","ts","jsx","js","mdx"]}}=j,v=j.config.basePath||"/",D=f(j.dir,t?.typescript?.tsconfigPath),{disable:P=!1,register:E=!0,dest:W=y,sw:$="sw.js",cacheStartUrl:S=!0,dynamicStartUrl:O=!0,dynamicStartUrlRedirect:R,publicExcludes:N=["!noprecache/**/*"],buildExcludes:C=[],fallbacks:L={},cacheOnFrontEndNav:M=!1,aggressiveFrontEndNavCaching:T=!1,reloadOnOnline:G=!0,scope:B=v,customWorkerDir:U,customWorkerSrc:F=U||"worker",customWorkerDest:H=W,customWorkerPrefix:I="worker",workboxOptions:Y={},extendDefaultRuntimeCaching:q=!1,swcMinify:K=t.swcMinify??h?.swcMinify??!1}=e;if("function"==typeof t.webpack&&(w=t.webpack(w,j)),P)return j.isServer&&V.info("PWA support is disabled."),w;let V$1=[];w.plugins||(w.plugins=[]),V.info(`Compiling for ${j.isServer?"server":"client (static)"}...`);let z=r.posix.join(v,$),J=r.posix.join(B,"/");w.plugins.push(new g.DefinePlugin({__PWA_SW__:`'${z}'`,__PWA_SCOPE__:`'${J}'`,__PWA_ENABLE_REGISTER__:`${!!E}`,__PWA_START_URL__:O?`'${v}'`:void 0,__PWA_CACHE_ON_FRONT_END_NAV__:`${!!M}`,__PWA_AGGRFEN_CACHE__:`${!!T}`,__PWA_RELOAD_ON_ONLINE__:`${!!G}`}));let Q=r.join(d,"sw-entry.js"),X=w.entry;if(w.entry=()=>X().then(i=>(i["main.js"]&&!i["main.js"].includes(Q)&&(Array.isArray(i["main.js"])?i["main.js"].unshift(Q):"string"==typeof i["main.js"]&&(i["main.js"]=[Q,i["main.js"]])),i["main-app"]&&!i["main-app"].includes(Q)&&(Array.isArray(i["main-app"])?i["main-app"].unshift(Q):"string"==typeof i["main-app"]&&(i["main-app"]=[Q,i["main-app"]])),i)),!j.isServer){setDefaultContext("shouldMinify",!x),setDefaultContext("useSwcMinify",K);let e=r.join(j.dir,W),o=r.join(j.dir,H),t=buildSWEntryWorker({isDev:x,destDir:e,shouldGenSWEWorker:M,basePath:v});w.plugins.push(new g.DefinePlugin({__PWA_SW_ENTRY_WORKER__:t&&`'${t}'`})),E||(V.info("Service worker won't be automatically registered as per the config, please call the following code in componentDidMount or useEffect:"),V.info(" window.workbox.register()"),D?.compilerOptions?.types?.includes("@ducanh2912/next-pwa/workbox")||V.info("You may also want to add @ducanh2912/next-pwa/workbox to compilerOptions.types in your tsconfig.json/jsconfig.json.")),V.info(`Service worker: ${r.join(e,$)}`),V.info(` URL: ${z}`),V.info(` Scope: ${J}`),w.plugins.push(new CleanWebpackPlugin({cleanOnceBeforeBuildPatterns:[r.join(e,"workbox-*.js"),r.join(e,"workbox-*.js.map"),r.join(e,$),r.join(e,`${$}.map`),r.join(e,"sw-chunks/**")]}));let d=buildCustomWorker({isDev:x,baseDir:j.dir,swDest:e,customWorkerSrc:F,customWorkerDest:o,customWorkerPrefix:I,plugins:w.plugins.filter(i=>i instanceof g.DefinePlugin),tsconfig:D,basePath:v});d&&V$1.unshift(d);let{additionalManifestEntries:h,modifyURLPrefix:y={},manifestTransforms:P=[],exclude:T,...G}=Y,B=h??[];B||(B=s$1.sync(["**/*","!workbox-*.js","!workbox-*.js.map","!worker-*.js","!worker-*.js.map","!fallback-*.js","!fallback-*.js.map",`!${$.replace(/^\/+/,"")}`,`!${$.replace(/^\/+/,"")}.map`,...N],{cwd:"public"}).map(e=>({url:r.posix.join(v,e),revision:getFileHash(`public/${e}`)}))),S&&(O?"string"==typeof R&&R.length>0&&B.push({url:R,revision:k}):B.push({url:v,revision:k})),Object.keys(G).forEach(i=>void 0===G[i]&&delete G[i]);let U=!1;if(L){L.document||(L.document=getDefaultDocumentPage(j.dir,A,b));let i=buildFallbackWorker({isDev:x,buildId:k,fallbacks:L,destDir:e,basePath:v});i&&(U=!0,V$1.unshift(i.name),i.precaches.forEach(i=>{i&&"boolean"!=typeof i&&!B.find(e=>"string"!=typeof e&&e.url.startsWith(i))&&B.push({url:i,revision:k});}));}let Q=resolveWorkboxCommon({dest:e,sw:$,dev:x,buildId:k,buildExcludes:C,manifestEntries:B,manifestTransforms:P,modifyURLPrefix:y,publicPath:w.output?.publicPath}),X=resolveWorkboxPlugin({rootDir:j.dir,basePath:v,isDev:x,workboxCommon:Q,workboxOptions:G,importScripts:V$1,extendDefaultRuntimeCaching:q,dynamicStartUrl:O,hasFallbacks:U});w.plugins.push(X);}return w}}));export{index as default,t as runtimeCaching}; +\ No newline at end of file diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 79adab3395..3375742263 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -4,7 +4,7 @@ import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' import type { Web3Provider } from '@ethersproject/providers' -import { FIREBASE_VAPID_KEY } from '@/services/firebase/app' +import { FIREBASE_VAPID_KEY, initializeFirebase } from '@/services/firebase/app' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' @@ -76,7 +76,9 @@ export const getRegisterDevicePayload = async ({ const [serviceWorkerRegistration] = await navigator.serviceWorker.getRegistrations() // Get Firebase token - const messaging = getMessaging() + const app = initializeFirebase() + const messaging = getMessaging(app) + const token = await getToken(messaging, { vapidKey: FIREBASE_VAPID_KEY, serviceWorkerRegistration, diff --git a/src/hooks/useFirebaseNotifications.ts b/src/hooks/useFirebaseNotifications.ts deleted file mode 100644 index 67b2874592..0000000000 --- a/src/hooks/useFirebaseNotifications.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react' - -import { initializeFirebase } from '@/services/firebase/app' - -export const useFirebaseNotifications = (): void => { - // TODO: Can we remove this? - // Register servicer worker - useEffect(() => { - // if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { - // return - // } - - const app = initializeFirebase() - - // if (app) { - // window.workbox.register() - // } - }, []) -} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 7a5e0110bc..d6612c4179 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,7 +37,6 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' -import { useFirebaseNotifications } from '@/hooks/useFirebaseNotifications' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -50,7 +49,6 @@ const InitApp = (): null => { useInitOnboard() useInitWeb3() useInitSafeCoreSDK() - useFirebaseNotifications() useTxNotifications() useSafeMessageNotifications() useSafeNotifications() diff --git a/src/worker/index.ts b/src/service-workers/firebase-messaging-sw.ts similarity index 74% rename from src/worker/index.ts rename to src/service-workers/firebase-messaging-sw.ts index 9384a15bfd..a7cbb508de 100644 --- a/src/worker/index.ts +++ b/src/service-workers/firebase-messaging-sw.ts @@ -1,6 +1,5 @@ // Be careful what you import here as it will increase the service worker bundle size -// TypeScript /// import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' @@ -8,21 +7,17 @@ import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' import { initializeFirebase } from '@/services/firebase/app' import { shouldShowNotification, parseFirebaseNotification } from '@/services/firebase/notifications' -// Default type of `self` is `WorkerGlobalScope & typeof globalThis` -// https://github.com/microsoft/TypeScript/issues/14877 -// TODO: Fix type -declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown; __WB_DISABLE_DEV_LOGS: boolean } +declare const self: ServiceWorkerGlobalScope -// Satisfy workbox -self.__WB_MANIFEST +export function firebaseMessagingSw() { + const ICON_PATH = '/images/safe-logo-green.png' -self.__WB_DISABLE_DEV_LOGS = true + const app = initializeFirebase() -const ICON_PATH = '/images/safe-logo-green.png' + if (!app) { + return + } -const app = initializeFirebase() - -if (app) { // Must be called before `onBackgroundMessage` as Firebase embeds a `notificationclick` listener self.addEventListener( 'notificationclick', diff --git a/src/service-workers/index.ts b/src/service-workers/index.ts new file mode 100644 index 0000000000..4a9cf17c76 --- /dev/null +++ b/src/service-workers/index.ts @@ -0,0 +1,7 @@ +// Be careful what you import here as it will increase the service worker bundle size + +/// + +import { firebaseMessagingSw } from './firebase-messaging-sw' + +firebaseMessagingSw() diff --git a/src/services/firebase/app.ts b/src/services/firebase/app.ts index 5918e77863..4c480e2c2f 100644 --- a/src/services/firebase/app.ts +++ b/src/services/firebase/app.ts @@ -3,7 +3,7 @@ import { initializeApp } from 'firebase/app' import type { FirebaseApp, FirebaseOptions } from 'firebase/app' -const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION +export const FIREBASE_IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' @@ -25,17 +25,19 @@ const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' -export const FIREBASE_VAPID_KEY = IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING +export const FIREBASE_VAPID_KEY = FIREBASE_IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING export const FIREBASE_OPTIONS: FirebaseOptions = { - apiKey: IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, - authDomain: IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, - databaseURL: IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, - projectId: IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, - storageBucket: IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, - messagingSenderId: IS_PRODUCTION ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION : FIREBASE_MESSAGING_SENDER_ID_STAGING, - appId: IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, - measurementId: IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, + apiKey: FIREBASE_IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, + authDomain: FIREBASE_IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, + databaseURL: FIREBASE_IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, + projectId: FIREBASE_IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, + storageBucket: FIREBASE_IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, + messagingSenderId: FIREBASE_IS_PRODUCTION + ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION + : FIREBASE_MESSAGING_SENDER_ID_STAGING, + appId: FIREBASE_IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, + measurementId: FIREBASE_IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, } as const export const initializeFirebase = () => { @@ -45,7 +47,7 @@ export const initializeFirebase = () => { return } - let app: FirebaseApp | null = null + let app: FirebaseApp | undefined try { app = initializeApp(FIREBASE_OPTIONS) diff --git a/src/services/firebase/notifications.ts b/src/services/firebase/notifications.ts index 7a48e3bf43..9e1474ac19 100644 --- a/src/services/firebase/notifications.ts +++ b/src/services/firebase/notifications.ts @@ -8,6 +8,7 @@ import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports import { isWebhookEvent, WebhookType } from './webhooks' import { getSafeNotificationPrefsKey, createNotificationUuidIndexedDb } from './preferences' +import { FIREBASE_IS_PRODUCTION } from './app' import type { WebhookEvent } from './webhooks' import type { NotificationPreferences, SafeNotificationPrefsKey } from './preferences' @@ -44,7 +45,7 @@ const GATEWAY_URL_PRODUCTION = process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' // localStorage cannot be accessed in service workers so we reference the flag from the environment -const GATEWAY_URL = !!process.env.NEXT_PUBLIC_IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING +const GATEWAY_URL = FIREBASE_IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING // XHR is not supported in service workers so we can't use the SDK // TODO: Migrate to SDK when we update it to use fetch diff --git a/tsconfig.json b/tsconfig.json index 51345ab3e1..045cbd3169 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,7 @@ "@/*": ["./src/*"], "@/public/*": ["./public/*"] }, - "plugins": [{ "name": "typescript-plugin-css-modules" }], - "types": ["@ducanh2912/next-pwa/workbox"] + "plugins": [{ "name": "typescript-plugin-css-modules" }] }, "include": ["next-env.d.ts", "src/definitions.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "src/types/contracts"] diff --git a/yarn.lock b/yarn.lock index 75f3adec15..4e4ea189f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,12 +49,12 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" - integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== dependencies: - "@babel/highlight" "^7.22.10" + "@babel/highlight" "^7.22.13" chalk "^2.4.2" "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": @@ -62,30 +62,30 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== -"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== "@babel/core@^7.11.1": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" - integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.15.tgz#15d4fd03f478a459015a4b94cfbb3bd42c48d2f4" + integrity sha512-PtZqMmgRrvj8ruoEOIwVA3yoF91O+Hgw9o7DAUTNBA6Mo2jpu31clx9a7Nz/9JznqetTR6zwfC4L3LAjKQXUwA== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helpers" "^7.22.10" - "@babel/parser" "^7.22.10" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.22.15" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.22.15" + "@babel/helpers" "^7.22.15" + "@babel/parser" "^7.22.15" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.22.15" + "@babel/types" "^7.22.15" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.2" + json5 "^2.2.3" semver "^6.3.1" "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.19.6": @@ -118,12 +118,12 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/generator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== +"@babel/generator@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339" + integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -151,11 +151,11 @@ "@babel/types" "^7.18.9" "@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz#573e735937e99ea75ea30788b57eb52fab7468c9" - integrity sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" "@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0": version "7.20.0" @@ -167,13 +167,13 @@ browserslist "^4.21.3" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== dependencies: "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" browserslist "^4.21.9" lru-cache "^5.1.1" semver "^6.3.1" @@ -191,15 +191,15 @@ "@babel/helper-replace-supers" "^7.19.1" "@babel/helper-split-export-declaration" "^7.18.6" -"@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3" - integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA== +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-replace-supers" "^7.22.9" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" @@ -215,9 +215,9 @@ regexpu-core "^5.2.1" "@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" - integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" regexpu-core "^5.3.1" @@ -300,19 +300,19 @@ dependencies: "@babel/types" "^7.18.9" -"@babel/helper-member-expression-to-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" - integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== +"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz#b95a144896f6d491ca7863576f820f3628818621" + integrity sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.22.15" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.22.15" "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": version "7.18.6" @@ -335,16 +335,16 @@ "@babel/traverse" "^7.20.1" "@babel/types" "^7.20.2" -"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" - integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== +"@babel/helper-module-transforms@^7.22.15", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.15.tgz#40ad2f6950f143900e9c1c72363c0b431a606082" + integrity sha512-l1UiX4UyHSFsYt17iQ3Se5pQQZZHa22zyIXURmvkmLCD4t/aU+dvNWHatKac/D9Vm9UES7nvIqHs4jZqKviUmQ== dependencies: "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" "@babel/helper-simple-access" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.15" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -466,20 +466,20 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== +"@babel/helper-validator-identifier@^7.22.15", "@babel/helper-validator-identifier@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044" + integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ== "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== -"@babel/helper-validator-option@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" - integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== "@babel/helper-wrap-function@^7.18.9": version "7.20.5" @@ -509,14 +509,14 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" - integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== +"@babel/helpers@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.15.tgz#f09c3df31e86e3ea0b7ff7556d85cdebd47ea6f1" + integrity sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw== dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.22.15" + "@babel/types" "^7.22.15" "@babel/highlight@^7.18.6": version "7.18.6" @@ -527,10 +527,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/highlight@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" - integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== +"@babel/highlight@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" + integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== dependencies: "@babel/helper-validator-identifier" "^7.22.5" chalk "^2.4.2" @@ -541,10 +541,10 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== -"@babel/parser@^7.22.10", "@babel/parser@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" - integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== +"@babel/parser@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.15.tgz#d34592bfe288a32e741aa0663dbc4829fcd55160" + integrity sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -553,10 +553,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" - integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -569,14 +569,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" "@babel/plugin-proposal-optional-chaining" "^7.18.9" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" - integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" "@babel/plugin-proposal-async-generator-functions@^7.20.1": version "7.20.1" @@ -888,10 +888,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-async-generator-functions@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8" - integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g== +"@babel/plugin-transform-async-generator-functions@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3" + integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w== dependencies: "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" @@ -937,10 +937,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-block-scoping@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" - integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== +"@babel/plugin-transform-block-scoping@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz#494eb82b87b5f8b1d8f6f28ea74078ec0a10a841" + integrity sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -952,12 +952,12 @@ "@babel/helper-create-class-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-class-static-block@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba" - integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" @@ -976,18 +976,18 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" - integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-function-name" "^7.22.5" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" @@ -1013,10 +1013,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-destructuring@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" - integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== +"@babel/plugin-transform-destructuring@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz#e7404ea5bb3387073b9754be654eecb578324694" + integrity sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -1050,10 +1050,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-dynamic-import@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e" - integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" @@ -1074,10 +1074,10 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-export-namespace-from@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b" - integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" @@ -1089,10 +1089,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-for-of@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" - integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -1114,10 +1114,10 @@ "@babel/helper-function-name" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-json-strings@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0" - integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-json-strings" "^7.8.3" @@ -1136,10 +1136,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-logical-assignment-operators@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c" - integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -1183,12 +1183,12 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-simple-access" "^7.19.4" -"@babel/plugin-transform-modules-commonjs@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" - integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== +"@babel/plugin-transform-modules-commonjs@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz#b11810117ed4ee7691b29bd29fd9f3f98276034f" + integrity sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg== dependencies: - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-simple-access" "^7.22.5" @@ -1202,13 +1202,13 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-identifier" "^7.19.1" -"@babel/plugin-transform-modules-systemjs@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496" - integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== +"@babel/plugin-transform-modules-systemjs@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1" + integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA== dependencies: "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.9" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-validator-identifier" "^7.22.5" @@ -1258,32 +1258,32 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381" - integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58" - integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1" - integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== dependencies: - "@babel/compat-data" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.5" + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-parameters" "^7.22.15" "@babel/plugin-transform-object-super@^7.18.6": version "7.18.6" @@ -1301,18 +1301,18 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-replace-supers" "^7.22.5" -"@babel/plugin-transform-optional-catch-binding@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333" - integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a" - integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g== +"@babel/plugin-transform-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz#d7a5996c2f7ca4ad2ad16dbb74444e5c4385b1ba" + integrity sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" @@ -1325,10 +1325,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-parameters@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" - integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== +"@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -1340,13 +1340,13 @@ "@babel/helper-create-class-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-private-property-in-object@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32" - integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" @@ -1574,16 +1574,16 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/preset-env@^7.11.0": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.10.tgz#3263b9fe2c8823d191d28e61eac60a79f9ce8a0f" - integrity sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.15.tgz#142716f8e00bc030dae5b2ac6a46fbd8b3e18ff8" + integrity sha512-tZFHr54GBkHk6hQuVA8w4Fmq+MSPsfvMG0vPnOYyTnJpyfMqybL8/MbNCPRT9zc2KBO2pe4tq15g6Uno4Jpoag== dependencies: "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" @@ -1604,41 +1604,41 @@ "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.10" + "@babel/plugin-transform-async-generator-functions" "^7.22.15" "@babel/plugin-transform-async-to-generator" "^7.22.5" "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.10" + "@babel/plugin-transform-block-scoping" "^7.22.15" "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.5" - "@babel/plugin-transform-classes" "^7.22.6" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.10" + "@babel/plugin-transform-destructuring" "^7.22.15" "@babel/plugin-transform-dotall-regex" "^7.22.5" "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.5" - "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" "@babel/plugin-transform-member-expression-literals" "^7.22.5" "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.5" - "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.15" + "@babel/plugin-transform-modules-systemjs" "^7.22.11" "@babel/plugin-transform-modules-umd" "^7.22.5" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" - "@babel/plugin-transform-numeric-separator" "^7.22.5" - "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.10" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + "@babel/plugin-transform-parameters" "^7.22.15" "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" "@babel/plugin-transform-property-literals" "^7.22.5" "@babel/plugin-transform-regenerator" "^7.22.10" "@babel/plugin-transform-reserved-words" "^7.22.5" @@ -1652,7 +1652,7 @@ "@babel/plugin-transform-unicode-regex" "^7.22.5" "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" babel-plugin-polyfill-corejs2 "^0.4.5" babel-plugin-polyfill-corejs3 "^0.8.3" babel-plugin-polyfill-regenerator "^0.5.2" @@ -1802,9 +1802,9 @@ regenerator-runtime "^0.13.11" "@babel/runtime@^7.11.2": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" - integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== dependencies: regenerator-runtime "^0.14.0" @@ -1831,14 +1831,14 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/template@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== +"@babel/template@^7.22.15", "@babel/template@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": version "7.20.5" @@ -1856,19 +1856,19 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" - integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== +"@babel/traverse@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.15.tgz#75be4d2d6e216e880e93017f4e2389aeb77ef2d9" + integrity sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ== dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.22.15" "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-function-name" "^7.22.5" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" debug "^4.1.0" globals "^11.1.0" @@ -1881,13 +1881,13 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@babel/types@^7.22.10", "@babel/types@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" - integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== +"@babel/types@^7.22.10", "@babel/types@^7.22.15", "@babel/types@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.15.tgz#266cb21d2c5fd0b3931e7a91b6dd72d2f617d282" + integrity sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA== dependencies: "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.15" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -6723,6 +6723,14 @@ array-back@^4.0.1, array-back@^4.0.2: resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -6787,6 +6795,19 @@ array.prototype.tosorted@^1.1.1: es-shim-unscopables "^1.0.0" get-intrinsic "^1.1.3" +arraybuffer.prototype.slice@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -7325,7 +7346,7 @@ browserify-sign@^4.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -browserslist@^4.14.5, browserslist@^4.21.9: +browserslist@^4.14.5, browserslist@^4.21.10, browserslist@^4.21.9: version "4.21.10" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== @@ -7963,11 +7984,11 @@ core-js-compat@^3.25.1: browserslist "^4.21.4" core-js-compat@^3.31.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90" - integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw== + version "3.32.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.1.tgz#55f9a7d297c0761a8eb1d31b593e0f5b6ffae964" + integrity sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA== dependencies: - browserslist "^4.21.9" + browserslist "^4.21.10" core-js-pure@^3.25.1: version "3.26.1" @@ -8399,6 +8420,14 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" @@ -8785,6 +8814,51 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: string.prototype.trimstart "^1.0.6" unbox-primitive "^1.0.2" +es-abstract@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" + integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.1" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.0" + safe-array-concat "^1.0.0" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.10" + es-get-iterator@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" @@ -8804,6 +8878,15 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -10165,7 +10248,7 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -10194,6 +10277,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -10313,6 +10406,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globalyzer@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" @@ -10457,6 +10557,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -10769,6 +10874,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + intl-messageformat@^9.13.0: version "9.13.0" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.13.0.tgz#97360b73bd82212e4f6005c712a4a16053165468" @@ -10804,6 +10918,15 @@ is-arguments@^1.0.4, is-arguments@^1.1.0, is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -11048,6 +11171,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.3: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -11823,7 +11953,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2: +json5@^2.1.3, json5@^2.2.0, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -12649,7 +12779,7 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@^13.4.19: +next@^13.2.0: version "13.4.19" resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== @@ -12767,6 +12897,11 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -13916,6 +14051,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -14257,6 +14401,16 @@ sade@^1.8.1: dependencies: mri "^1.1.0" +safe-array-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -14800,7 +14954,21 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.6: + version "4.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.9.tgz#148779de0f75d36b13b15885fec5cadde994520d" + integrity sha512-6i5hL3MqG/K2G43mWXWgP+qizFW/QH/7kCNN13JrJS5q48FN5IKksLDscexKP3dnmB6cdm9jlNgAsWNLpSykmA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + side-channel "^1.0.4" + +string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== @@ -14814,6 +14982,15 @@ string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -15098,7 +15275,17 @@ terser-webpack-plugin@5.3.9, terser-webpack-plugin@^5.3.7: serialize-javascript "^6.0.1" terser "^5.16.8" -terser@^5.0.0, terser@^5.16.8: +terser@^5.0.0: + version "5.19.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.4.tgz#941426fa482bf9b40a0308ab2b3cd0cf7c775ebd" + integrity sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +terser@^5.16.8: version "5.19.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== @@ -15477,6 +15664,45 @@ typechain@^8.0.0: ts-command-line-args "^2.2.0" ts-essentials "^7.0.1" +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray-to-buffer@3.1.5, typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -16265,6 +16491,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.10, which-typed-array@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.2, which-typed-array@^1.1.8: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" From e227b5f45386541b9e90bc6bdfc0b7d1c1f846d4 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 14:37:59 +0200 Subject: [PATCH 36/62] chore: convert env var to JSON --- .github/workflows/build/action.yml | 18 ++----------- README.md | 18 ++----------- src/services/firebase/app.ts | 43 +++++++----------------------- 3 files changed, 13 insertions(+), 66 deletions(-) diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index b43a71369e..fd57aada4f 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,21 +43,7 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} - NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION }} + NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} - NEXT_PUBLIC_FIREBASE_API_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_API_KEY_STAGING }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING }} - NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING }} - NEXT_PUBLIC_FIREBASE_APP_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_APP_ID_STAGING }} - NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING }} diff --git a/README.md b/README.md index d3ceb649b0..c2bdf1146f 100644 --- a/README.md +++ b/README.md @@ -42,23 +42,9 @@ Here's the list of all the required and optional variables: | `NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING` | optional | Relay URL on staging | `NEXT_PUBLIC_IS_OFFICIAL_HOST` | optional | Whether it's the official distribution of the app, or a fork; has legal implications. Set to true only if you also update the legal pages like Imprint and Terms of use | `NEXT_PUBLIC_REDEFINE_API` | optional | Redefine API base URL -| `NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION` | optional | Firebase Cloud Messaging (FCM) API key on production -| `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION` | optional | FCM auth domain on production -| `NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION` | optional | FCM database URL on production -| `NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION` | optional | FCM project ID on production -| `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION` | optional | FCM storage bucket on production -| `NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION` | optional | FCM sender ID on production -| `NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION` | optional | FCM app ID on production -| `NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION` | optional | FCM measurement ID on production +| `NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION` | optional | Firebase Cloud Messaging (FCM) `initializeApp` options on production | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | optional | FCM vapid key on production -| `NEXT_PUBLIC_FIREBASE_API_KEY_STAGING` | optional | FCM API key on staging -| `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING` | optional | FCM auth domain on staging -| `NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING` | optional | FCM database URL on staging -| `NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING` | optional | FCM project ID on staging -| `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING` | optional | FCM storage bucket on staging -| `NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING` | optional | FCM sender ID on staging -| `NEXT_PUBLIC_FIREBASE_APP_ID_STAGING` | optional | FCM app ID on staging -| `NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING` | optional | FCM measurement ID on staging +| `NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING` | optional | FCM `initializeApp` options on staging | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | optional | FCM vapid key on staging If you don't provide some of the optional vars, the corresponding features will be disabled in the UI. diff --git a/src/services/firebase/app.ts b/src/services/firebase/app.ts index 4c480e2c2f..bb29d81cf7 100644 --- a/src/services/firebase/app.ts +++ b/src/services/firebase/app.ts @@ -5,40 +5,15 @@ import type { FirebaseApp, FirebaseOptions } from 'firebase/app' export const FIREBASE_IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION -const FIREBASE_API_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION || '' -const FIREBASE_AUTH_DOMAIN_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION || '' -const FIREBASE_DATABASE_URL_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION || '' -const FIREBASE_PROJECT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION || '' -const FIREBASE_STORAGE_BUCKET_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION || '' -const FIREBASE_MESSAGING_SENDER_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION || '' -const FIREBASE_APP_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION || '' -const FIREBASE_MEASUREMENT_ID_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION || '' -const FIREBASE_VAPID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' - -const FIREBASE_API_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_API_KEY_STAGING || '' -const FIREBASE_AUTH_DOMAIN_STAGING = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING || '' -const FIREBASE_DATABASE_URL_STAGING = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING || '' -const FIREBASE_PROJECT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING || '' -const FIREBASE_STORAGE_BUCKET_STAGING = process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING || '' -const FIREBASE_MESSAGING_SENDER_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING || '' -const FIREBASE_APP_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_APP_ID_STAGING || '' -const FIREBASE_MEASUREMENT_ID_STAGING = process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING || '' -const FIREBASE_VAPID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' - -export const FIREBASE_VAPID_KEY = FIREBASE_IS_PRODUCTION ? FIREBASE_VAPID_KEY_PRODUCTION : FIREBASE_VAPID_KEY_STAGING - -export const FIREBASE_OPTIONS: FirebaseOptions = { - apiKey: FIREBASE_IS_PRODUCTION ? FIREBASE_API_KEY_PRODUCTION : FIREBASE_API_KEY_STAGING, - authDomain: FIREBASE_IS_PRODUCTION ? FIREBASE_AUTH_DOMAIN_PRODUCTION : FIREBASE_AUTH_DOMAIN_STAGING, - databaseURL: FIREBASE_IS_PRODUCTION ? FIREBASE_DATABASE_URL_PRODUCTION : FIREBASE_DATABASE_URL_STAGING, - projectId: FIREBASE_IS_PRODUCTION ? FIREBASE_PROJECT_ID_PRODUCTION : FIREBASE_PROJECT_ID_STAGING, - storageBucket: FIREBASE_IS_PRODUCTION ? FIREBASE_STORAGE_BUCKET_PRODUCTION : FIREBASE_STORAGE_BUCKET_STAGING, - messagingSenderId: FIREBASE_IS_PRODUCTION - ? FIREBASE_MESSAGING_SENDER_ID_PRODUCTION - : FIREBASE_MESSAGING_SENDER_ID_STAGING, - appId: FIREBASE_IS_PRODUCTION ? FIREBASE_APP_ID_PRODUCTION : FIREBASE_APP_ID_STAGING, - measurementId: FIREBASE_IS_PRODUCTION ? FIREBASE_MEASUREMENT_ID_PRODUCTION : FIREBASE_MEASUREMENT_ID_STAGING, -} as const +export const FIREBASE_VAPID_KEY = FIREBASE_IS_PRODUCTION + ? process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' + : process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' + +export const FIREBASE_OPTIONS: FirebaseOptions = JSON.parse( + FIREBASE_IS_PRODUCTION + ? process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION || '' + : process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING || '', +) export const initializeFirebase = () => { const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) From 75b5a63d8189dbe37b42ffb469b1bb2361a2d28a Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 15:16:59 +0200 Subject: [PATCH 37/62] refactor: abstract code from component + fix test --- .../GlobalPushNotifications.tsx | 260 +++++++++++------- .../settings/PushNotifications/logic.test.ts | 2 +- src/services/firebase/app.ts | 22 +- 3 files changed, 181 insertions(+), 103 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index b99bc888e0..d3ba55b95a 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -23,7 +23,7 @@ import { useNotificationRegistrations } from './hooks/useNotificationRegistratio import { selectAllAddedSafes } from '@/store/addedSafesSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' -// import { requestNotificationPermission } from './logic' +import { requestNotificationPermission } from './logic' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { NotificationPreferences } from '@/services/firebase/preferences' @@ -31,30 +31,30 @@ import CheckWallet from '@/components/common/CheckWallet' import css from './styles.module.css' +// UI logic + // Convert data structure of added Safes export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafes => { - const obj: NotifiableSafes = {} - - for (const [chainId, addedSafesOnChain] of Object.entries(addedSafes)) { - obj[chainId] = Object.keys(addedSafesOnChain) - } - - return obj + return Object.entries(addedSafes).reduce((acc, [chainId, addedSafesOnChain]) => { + acc[chainId] = Object.keys(addedSafesOnChain) + return acc + }, {}) } // Convert data structure of currently notified Safes -const transformCurrentSubscribedSafes = (allPreferences: NotificationPreferences): NotifiableSafes => { - const obj: NotifiableSafes = {} +const transformCurrentSubscribedSafes = (allPreferences?: NotificationPreferences): NotifiableSafes | undefined => { + if (!allPreferences) { + return + } - for (const { chainId, safeAddress } of Object.values(allPreferences)) { - if (!obj[chainId]) { - obj[chainId] = [] + return Object.values(allPreferences).reduce((acc, { chainId, safeAddress }) => { + if (!acc[chainId]) { + acc[chainId] = [] } - obj[chainId].push(safeAddress) - } - - return obj + acc[chainId].push(safeAddress) + return acc + }, {}) } // Merges added Safes and currently notified Safes into a single data structure without duplicates @@ -76,6 +76,129 @@ const mergeNotifiableSafes = (addedSafes: AddedSafesState, currentSubscriptions? return notifiableSafes } +const getTotalNotifiableSafes = (notifiableSafes: NotifiableSafes): number => { + return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { + return (acc += safeAddresses.length) + }, 0) +} + +const areAllSafesSelected = (notifiableSafes: NotifiableSafes, selectedSafes: NotifiableSafes): boolean => { + return Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { + const hasChain = Object.keys(selectedSafes).includes(chainId) + const hasEverySafe = safeAddresses?.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) + return hasChain && hasEverySafe + }) +} + +// Total number of signatures required to register selected Safes +const getTotalSignaturesRequired = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes): number => { + if (!currentNotifiedSafes) { + return 0 + } + + return Object.keys(selectedSafes).filter((chainId) => { + return !Object.keys(currentNotifiedSafes).includes(chainId) + }).length +} + +const shouldRegisterSelectedSafes = ( + selectedSafes: NotifiableSafes, + currentNotifiedSafes?: NotifiableSafes, +): boolean => { + if (!currentNotifiedSafes) { + return false + } + + return Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { + return safeAddresses.some((safeAddress) => !currentNotifiedSafes[chainId]?.includes(safeAddress)) + }) +} + +const shouldUnregsiterSelectedSafes = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes) => { + if (!currentNotifiedSafes) { + return false + } + + return Object.entries(currentNotifiedSafes).some(([chainId, safeAddresses]) => { + return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) + }) +} + +// onSave logic + +// Safes that need to be registered with the service +const getSafesToRegister = ( + selectedSafes: NotifiableSafes, + currentNotifiedSafes?: NotifiableSafes, +): NotifiableSafes | undefined => { + const safesToRegister = Object.entries(selectedSafes).reduce((acc, [chainId, safeAddresses]) => { + const safesToRegisterOnChain = safeAddresses.filter( + (safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress), + ) + + if (safesToRegisterOnChain.length > 0) { + acc[chainId] = safeAddresses + } + + return acc + }, {}) + + const shouldRegister = Object.values(safesToRegister).some((safeAddresses) => safeAddresses.length > 0) + + if (shouldRegister) { + return safesToRegister + } +} + +// Safes that need to be unregistered with the service +const getSafesToUnregister = ( + selectedSafes: NotifiableSafes, + currentNotifiedSafes?: NotifiableSafes, +): NotifiableSafes | undefined => { + if (!currentNotifiedSafes) { + return + } + + const safesToUnregister = Object.entries(currentNotifiedSafes).reduce( + (acc, [chainId, safeAddresses]) => { + const safesToUnregisterOnChain = safeAddresses.filter( + (safeAddress) => !selectedSafes[chainId]?.includes(safeAddress), + ) + + if (safesToUnregisterOnChain.length > 0) { + acc[chainId] = safeAddresses + } + return acc + }, + {}, + ) + + const shouldUnregister = Object.values(safesToUnregister).some((safeAddresses) => safeAddresses.length > 0) + + if (shouldUnregister) { + return safesToUnregister + } +} + +// Whether the device needs to be unregistered from the service +const shouldUnregisterDevice = ( + chainId: string, + safeAddresses: Array, + currentNotifiedSafes?: NotifiableSafes, +): boolean => { + if (!currentNotifiedSafes) { + return false + } + + if (safeAddresses.length !== currentNotifiedSafes[chainId].length) { + return false + } + + return safeAddresses.every((safeAddress) => { + return currentNotifiedSafes[chainId]?.includes(safeAddress) + }) +} + export const GlobalPushNotifications = (): ReactElement | null => { const chains = useChains() const addedSafes = useAppSelector(selectAllAddedSafes) @@ -86,17 +209,12 @@ export const GlobalPushNotifications = (): ReactElement | null => { // Safes selected in the UI const [selectedSafes, setSelectedSafes] = useState({}) - const selectedChains = Object.keys(selectedSafes) // Current Safes registered for notifications in indexedDB const currentNotifiedSafes = useMemo(() => { const allPreferences = getAllPreferences() - if (!allPreferences) { - return - } return transformCurrentSubscribedSafes(allPreferences) }, [getAllPreferences]) - const currentNotifiedChains = currentNotifiedSafes ? Object.keys(currentNotifiedSafes) : [] // `currentNotifiedSafes` is initially undefined until indexedDB resolves useEffect(() => { @@ -117,16 +235,12 @@ export const GlobalPushNotifications = (): ReactElement | null => { }, [addedSafes, currentNotifiedSafes]) const totalNotifiableSafes = useMemo(() => { - return Object.values(notifiableSafes).reduce((acc, safeAddresses) => { - return (acc += safeAddresses.length) - }, 0) + return getTotalNotifiableSafes(notifiableSafes) }, [notifiableSafes]) - const isAllSelected = Object.entries(notifiableSafes).every(([chainId, safeAddresses]) => { - const hasChain = selectedChains.includes(chainId) - const hasEverySafe = safeAddresses?.every((safeAddress) => selectedSafes[chainId]?.includes(safeAddress)) - return hasChain && hasEverySafe - }) + const isAllSelected = useMemo(() => { + return areAllSafesSelected(notifiableSafes, selectedSafes) + }, [notifiableSafes, selectedSafes]) const onSelectAll = () => { setSelectedSafes(() => { @@ -143,85 +257,45 @@ 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)) - }) + const totalSignaturesRequired = useMemo(() => { + return getTotalSignaturesRequired(selectedSafes, currentNotifiedSafes) + }, [currentNotifiedSafes, selectedSafes]) - const shouldUnregisterUnselectedSafes = - currentNotifiedSafes && - Object.entries(currentNotifiedSafes).some(([chainId, safeAddresses]) => { - return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) - }) - - const canSave = shouldRegisterSelectedSafes || shouldUnregisterUnselectedSafes + const canSave = useMemo(() => { + return ( + shouldRegisterSelectedSafes(selectedSafes, currentNotifiedSafes) || + shouldUnregsiterSelectedSafes(selectedSafes, currentNotifiedSafes) + ) + }, [selectedSafes, currentNotifiedSafes]) const onSave = async () => { if (!canSave) { return } - // TODO: Can we remove this? - // const isGranted = await requestNotificationPermission() + const isGranted = await requestNotificationPermission() - // if (!isGranted) { - // return - // } + if (!isGranted) { + return + } const registrationPromises: Array> = [] - const safesToRegister = Object.entries(selectedSafes).reduce((acc, [chainId, safeAddresses]) => { - const safesToRegisterOnChain = safeAddresses.filter( - (safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress), - ) - - if (safesToRegisterOnChain.length > 0) { - acc[chainId] = safeAddresses - } - - return acc - }, {}) - - const shouldRegister = Object.values(safesToRegister).some((safeAddresses) => safeAddresses.length > 0) - - if (shouldRegister) { + const safesToRegister = getSafesToRegister(selectedSafes, currentNotifiedSafes) + if (safesToRegister) { registrationPromises.push(registerNotifications(safesToRegister)) } - const shouldUnregister = - currentNotifiedSafes && - Object.entries(currentNotifiedSafes).reduce((acc, [chainId, safeAddresses]) => { - const safesToUnregisterOnChain = safeAddresses.filter( - (safeAddress) => !selectedSafes[chainId]?.includes(safeAddress), - ) - - if (safesToUnregisterOnChain.length > 0) { - acc[chainId] = safeAddresses - } - return acc - }, {}) - - if (shouldUnregister) { - for (const [chainId, safeAddresses] of Object.entries(shouldUnregister)) { - const shouldUnregsiterDevice = - safeAddresses.length === currentNotifiedSafes[chainId].length && - safeAddresses.every((safeAddress) => { - return currentNotifiedSafes[chainId]?.includes(safeAddress) - }) - - if (shouldUnregsiterDevice) { - registrationPromises.push(unregisterChainNotifications(chainId)) - continue + const safesToUnregister = getSafesToUnregister(selectedSafes, currentNotifiedSafes) + if (safesToUnregister) { + const unregistrationPromises = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { + if (shouldUnregisterDevice(chainId, safeAddresses, currentNotifiedSafes)) { + return unregisterChainNotifications(chainId) } + return safeAddresses.map((safeAddress) => unregisterSafeNotifications(chainId, safeAddress)) + }) - for (const safeAddress of safeAddresses) { - registrationPromises.push(unregisterSafeNotifications(chainId, safeAddress)) - } - } + registrationPromises.push(...unregistrationPromises) } await Promise.all(registrationPromises) diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts index f6f55d85ba..e217ef05cd 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/logic.test.ts @@ -18,7 +18,7 @@ Object.defineProperty(globalThis, 'crypto', { Object.defineProperty(globalThis, 'navigator', { value: { serviceWorker: { - getRegistration: jest.fn(), + getRegistrations: () => [], }, }, }) diff --git a/src/services/firebase/app.ts b/src/services/firebase/app.ts index bb29d81cf7..4dd1f1f6d5 100644 --- a/src/services/firebase/app.ts +++ b/src/services/firebase/app.ts @@ -5,15 +5,19 @@ import type { FirebaseApp, FirebaseOptions } from 'firebase/app' export const FIREBASE_IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION -export const FIREBASE_VAPID_KEY = FIREBASE_IS_PRODUCTION - ? process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' - : process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING || '' - -export const FIREBASE_OPTIONS: FirebaseOptions = JSON.parse( - FIREBASE_IS_PRODUCTION - ? process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION || '' - : process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING || '', -) +const FIREBASE_VALID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' +const FIREBASE_VALID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING +export const FIREBASE_VAPID_KEY = FIREBASE_IS_PRODUCTION ? FIREBASE_VALID_KEY_PRODUCTION : FIREBASE_VALID_KEY_STAGING + +export const FIREBASE_OPTIONS: FirebaseOptions = (() => { + const FIREBASE_OPTIONS_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION || '' + const FIREBASE_OPTIONS_STAGING = process.env.NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING || '' + try { + return JSON.parse(FIREBASE_IS_PRODUCTION ? FIREBASE_OPTIONS_PRODUCTION : FIREBASE_OPTIONS_STAGING) + } catch { + return {} + } +})() export const initializeFirebase = () => { const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) From ac3d9e2ccb61578bf496e0997d0ad1556dcf29d8 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 16:33:07 +0200 Subject: [PATCH 38/62] fix: env issue --- .../GlobalPushNotifications.tsx | 18 +++--------------- src/config/constants.ts | 4 ++-- src/services/firebase/app.ts | 2 +- src/services/firebase/notifications.ts | 4 ++-- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index d3ba55b95a..f531e78610 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -92,12 +92,8 @@ const areAllSafesSelected = (notifiableSafes: NotifiableSafes, selectedSafes: No // Total number of signatures required to register selected Safes const getTotalSignaturesRequired = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes): number => { - if (!currentNotifiedSafes) { - return 0 - } - return Object.keys(selectedSafes).filter((chainId) => { - return !Object.keys(currentNotifiedSafes).includes(chainId) + return !Object.keys(currentNotifiedSafes || {}).includes(chainId) }).length } @@ -105,21 +101,13 @@ const shouldRegisterSelectedSafes = ( selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes, ): boolean => { - if (!currentNotifiedSafes) { - return false - } - return Object.entries(selectedSafes).some(([chainId, safeAddresses]) => { - return safeAddresses.some((safeAddress) => !currentNotifiedSafes[chainId]?.includes(safeAddress)) + return safeAddresses.some((safeAddress) => !currentNotifiedSafes?.[chainId]?.includes(safeAddress)) }) } const shouldUnregsiterSelectedSafes = (selectedSafes: NotifiableSafes, currentNotifiedSafes?: NotifiableSafes) => { - if (!currentNotifiedSafes) { - return false - } - - return Object.entries(currentNotifiedSafes).some(([chainId, safeAddresses]) => { + return Object.entries(currentNotifiedSafes || {}).some(([chainId, safeAddresses]) => { return safeAddresses.some((safeAddress) => !selectedSafes[chainId]?.includes(safeAddress)) }) } diff --git a/src/config/constants.ts b/src/config/constants.ts index a148aa3caa..16c50fe958 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,6 +1,6 @@ import chains from './chains' -export const IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION +export const IS_PRODUCTION = process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true' export const IS_DEV = process.env.NODE_ENV === 'development' export const GATEWAY_URL_PRODUCTION = @@ -91,7 +91,7 @@ export const DISCORD_URL = 'https://chat.safe.global' export const TWITTER_URL = 'https://twitter.com/safe' // Legal -export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST || false +export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST === 'true' || false // Risk mitigation (Redefine) export const REDEFINE_SIMULATION_URL = 'https://dashboard.redefine.net/reports/' diff --git a/src/services/firebase/app.ts b/src/services/firebase/app.ts index 4dd1f1f6d5..452d8323bf 100644 --- a/src/services/firebase/app.ts +++ b/src/services/firebase/app.ts @@ -3,7 +3,7 @@ import { initializeApp } from 'firebase/app' import type { FirebaseApp, FirebaseOptions } from 'firebase/app' -export const FIREBASE_IS_PRODUCTION = !!process.env.NEXT_PUBLIC_IS_PRODUCTION +export const FIREBASE_IS_PRODUCTION = process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true' const FIREBASE_VALID_KEY_PRODUCTION = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION || '' const FIREBASE_VALID_KEY_STAGING = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING diff --git a/src/services/firebase/notifications.ts b/src/services/firebase/notifications.ts index 9e1474ac19..401a096ad8 100644 --- a/src/services/firebase/notifications.ts +++ b/src/services/firebase/notifications.ts @@ -7,7 +7,7 @@ import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports import { isWebhookEvent, WebhookType } from './webhooks' -import { getSafeNotificationPrefsKey, createNotificationUuidIndexedDb } from './preferences' +import { getSafeNotificationPrefsKey, createNotificationPrefsIndexedDb } from './preferences' import { FIREBASE_IS_PRODUCTION } from './app' import type { WebhookEvent } from './webhooks' import type { NotificationPreferences, SafeNotificationPrefsKey } from './preferences' @@ -28,7 +28,7 @@ export const shouldShowNotification = async (payload: MessagePayload): Promise(key, store).catch( () => null, From f7047d80e383144c702dfed63007afb2f6f3e793 Mon Sep 17 00:00:00 2001 From: iamacook Date: Wed, 6 Sep 2023 17:08:27 +0200 Subject: [PATCH 39/62] refactor: leverage SDK --- package.json | 2 +- .../settings/PushNotifications/logic.ts | 4 +- .../{index.test.ts => notifications.test.ts} | 524 +++++++----------- src/services/firebase/notifications.ts | 41 +- yarn.lock | 10 +- 5 files changed, 232 insertions(+), 349 deletions(-) rename src/services/firebase/__tests__/{index.test.ts => notifications.test.ts} (63%) diff --git a/package.json b/package.json index 0bc4f6a45d..072fa218fa 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@safe-global/safe-core-sdk-utils": "^1.7.4", "@safe-global/safe-deployments": "1.25.0", "@safe-global/safe-ethers-lib": "^1.9.4", - "@safe-global/safe-gateway-typescript-sdk": "^3.10.0", + "@safe-global/safe-gateway-typescript-sdk": "^3.11.0", "@safe-global/safe-modules-deployments": "^1.0.0", "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.28.1", diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 3375742263..ec8fb31803 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -1,7 +1,7 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { getToken, getMessaging } from 'firebase/messaging' -import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' -import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk/dist/types/notifications' +import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk' +import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk' import type { Web3Provider } from '@ethersproject/providers' import { FIREBASE_VAPID_KEY, initializeFirebase } from '@/services/firebase/app' diff --git a/src/services/firebase/__tests__/index.test.ts b/src/services/firebase/__tests__/notifications.test.ts similarity index 63% rename from src/services/firebase/__tests__/index.test.ts rename to src/services/firebase/__tests__/notifications.test.ts index 5f5f0662ec..1b220d63ce 100644 --- a/src/services/firebase/__tests__/index.test.ts +++ b/src/services/firebase/__tests__/notifications.test.ts @@ -1,5 +1,5 @@ import { hexZeroPad } from 'ethers/lib/utils' -import type { ChainInfo, TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' import { _parseWebhookNotification } from '../notifications' import { WebhookType } from '../webhooks' @@ -16,13 +16,7 @@ import type { SafeCreatedEvent, } from '../webhooks' -const setupFetchStub = (data: any) => (_url: string) => { - return Promise.resolve({ - json: () => Promise.resolve(data), - status: 200, - ok: true, - }) -} +jest.mock('@safe-global/safe-gateway-typescript-sdk') Object.defineProperty(self, 'location', { value: { @@ -31,8 +25,12 @@ Object.defineProperty(self, 'location', { }) describe('parseWebhookNotification', () => { + let getChainsConfigSpy: jest.SpyInstance> + let getBalancesMockSpy: jest.SpyInstance> + beforeEach(() => { - global.fetch = jest.fn() + getChainsConfigSpy = jest.spyOn(sdk, 'getChainsConfig') + getBalancesMockSpy = jest.spyOn(sdk, 'getBalances') }) describe('should parse NEW_CONFIRMATION payloads', () => { @@ -45,11 +43,9 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification(payload) @@ -61,7 +57,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) @@ -84,11 +80,9 @@ describe('parseWebhookNotification', () => { describe('successful transactions', () => { it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification({ ...payload, @@ -103,7 +97,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains const notification = await _parseWebhookNotification({ ...payload, @@ -120,11 +114,9 @@ describe('parseWebhookNotification', () => { describe('failed transactions', () => { it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification({ ...payload, @@ -139,7 +131,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains const notification = await _parseWebhookNotification({ ...payload, @@ -164,11 +156,9 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification(payload) @@ -180,7 +170,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) @@ -202,18 +192,16 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce( - setupFetchStub({ - results: [ - { - chainName: 'Polygon', - chainId: payload.chainId, - shortName: 'matic', - nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, - } as ChainInfo, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [ + { + chainName: 'Polygon', + chainId: payload.chainId, + shortName: 'matic', + nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, + } as sdk.ChainInfo, + ], + }) const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ @@ -224,7 +212,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) @@ -246,18 +234,16 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce( - setupFetchStub({ - results: [ - { - chainName: 'Polygon', - chainId: payload.chainId, - shortName: 'matic', - nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, - } as ChainInfo, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [ + { + chainName: 'Polygon', + chainId: payload.chainId, + shortName: 'matic', + nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, + } as sdk.ChainInfo, + ], + }) const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ @@ -268,6 +254,8 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains + const notification = await _parseWebhookNotification(payload) expect(notification).toEqual({ @@ -293,27 +281,21 @@ describe('parseWebhookNotification', () => { } it('with chain and token info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const notification = await _parseWebhookNotification(payload) @@ -323,27 +305,21 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', }) - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -355,23 +331,19 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const notification = await _parseWebhookNotification(payload) @@ -381,23 +353,19 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global', }) - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -409,14 +377,10 @@ describe('parseWebhookNotification', () => { }) it('without token info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const notification = await _parseWebhookNotification(payload) @@ -426,14 +390,10 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', }) - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -445,10 +405,8 @@ describe('parseWebhookNotification', () => { }) it('without chain and balance info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const notification = await _parseWebhookNotification(payload) @@ -458,10 +416,8 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global', }) - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -488,27 +444,21 @@ describe('parseWebhookNotification', () => { } it('with chain and token info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const notification = await _parseWebhookNotification(payload) @@ -518,27 +468,21 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', }) - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -550,18 +494,12 @@ describe('parseWebhookNotification', () => { }) it('with chain and empty token info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [], // Transaction sent all of the tokens - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [] as sdk.SafeBalanceResponse['items'], // Transaction sent all of the tokens + } as sdk.SafeBalanceResponse) const notification = await _parseWebhookNotification(payload) @@ -571,27 +509,21 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', }) - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -603,23 +535,19 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const notification = await _parseWebhookNotification(payload) @@ -629,23 +557,19 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global', }) - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce( - setupFetchStub({ - items: [ - { - tokenInfo: { - address: payload.tokenAddress, - decimals: 18, - name: 'Fake', - symbol: 'FAKE', - } as TokenInfo, - }, - ], - }), - ) + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockResolvedValue({ + items: [ + { + tokenInfo: { + address: payload.tokenAddress, + decimals: 18, + name: 'Fake', + symbol: 'FAKE', + }, + }, + ], + } as sdk.SafeBalanceResponse) const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -656,14 +580,10 @@ describe('parseWebhookNotification', () => { }) }) it('without token info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const notification = await _parseWebhookNotification(payload) @@ -673,14 +593,10 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', }) - global.fetch = jest - .fn() - .mockImplementationOnce( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as ChainInfo], - }), - ) - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -692,10 +608,8 @@ describe('parseWebhookNotification', () => { }) it('without chain and balance info', async () => { - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const notification = await _parseWebhookNotification(payload) @@ -705,10 +619,8 @@ describe('parseWebhookNotification', () => { link: 'https://app.safe.global', }) - global.fetch = jest - .fn() - .mockImplementationOnce(() => Promise.reject()) // chains - .mockImplementationOnce(() => Promise.reject()) // tokens + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const erc20Notification = await _parseWebhookNotification(erc20Payload) @@ -730,11 +642,9 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification(payload) @@ -746,7 +656,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) @@ -767,11 +677,9 @@ describe('parseWebhookNotification', () => { } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification(payload) @@ -783,7 +691,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) @@ -804,11 +712,9 @@ describe('parseWebhookNotification', () => { blockNumber: '1', } it('with chain info', async () => { - global.fetch = jest.fn().mockImplementation( - setupFetchStub({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as ChainInfo], - }), - ) + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) const notification = await _parseWebhookNotification(payload) @@ -816,7 +722,7 @@ describe('parseWebhookNotification', () => { }) it('without chain info', async () => { - global.fetch = jest.fn().mockImplementationOnce(() => Promise.reject()) // chains + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains const notification = await _parseWebhookNotification(payload) diff --git a/src/services/firebase/notifications.ts b/src/services/firebase/notifications.ts index 401a096ad8..3352dc94a6 100644 --- a/src/services/firebase/notifications.ts +++ b/src/services/firebase/notifications.ts @@ -2,7 +2,8 @@ import { get as getFromIndexedDb } from 'idb-keyval' import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable -import type { ChainInfo, ChainListResponse, SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' +import { getChainsConfig, getBalances, setBaseUrl } from '@safe-global/safe-gateway-typescript-sdk' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports @@ -46,22 +47,12 @@ const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'http // localStorage cannot be accessed in service workers so we reference the flag from the environment const GATEWAY_URL = FIREBASE_IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING +setBaseUrl(GATEWAY_URL) -// XHR is not supported in service workers so we can't use the SDK -// TODO: Migrate to SDK when we update it to use fetch const getChain = async (chainId: string): Promise => { - const ENDPOINT = `${GATEWAY_URL}/v1/chains` - - let chains: ChainListResponse | null = null - - try { - const response = await fetch(ENDPOINT) - if (response.ok) { - chains = await response.json() - } - } catch {} - - return chains?.results.find((chain) => chain.chainId === chainId) + return getChainsConfig() + .then(({ results }) => results.find((chain) => chain.chainId === chainId)) + .catch(() => undefined) } const getTokenInfo = async ( @@ -71,7 +62,6 @@ const getTokenInfo = async ( tokenValue?: string, ): Promise<{ symbol: string; value: string; name: string }> => { const DEFAULT_CURRENCY = 'USD' - const ENDPOINT = `${GATEWAY_URL}/v1/chains/${chainId}/safes/${safeAddress}/balances/${DEFAULT_CURRENCY}` const DEFAULT_INFO = { symbol: 'tokens', @@ -79,16 +69,9 @@ const getTokenInfo = async ( name: 'Token', } - let balances: SafeBalanceResponse | null = null - - try { - const response = await fetch(ENDPOINT) - if (response.ok) { - balances = await response.json() - } - } catch {} - - const tokenInfo = balances?.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo + const tokenInfo = await getBalances(chainId, safeAddress, DEFAULT_CURRENCY) + .then(({ items }) => items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo) + .catch(() => null) if (!tokenInfo) { return DEFAULT_INFO @@ -132,11 +115,7 @@ type NotificationsMap = { export const _parseWebhookNotification = async ( data: WebhookEvent, ): Promise<{ title: string; body: string; link: string } | undefined> => { - let chain: ChainInfo | undefined - - try { - chain = await getChain(data.chainId) - } catch {} + const chain = await getChain(data.chainId) const chainName = chain?.chainName ?? `chain ${data.chainId}` diff --git a/yarn.lock b/yarn.lock index 4e4ea189f1..699cefdfe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4530,12 +4530,10 @@ "@safe-global/safe-core-sdk-utils" "^1.7.4" ethers "5.7.2" -"@safe-global/safe-gateway-typescript-sdk@^3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.10.0.tgz#a252ac5a61487d7785c44f1ed7e899ccd5aa9038" - integrity sha512-nhWjFRRgrGz4uZbyQ3Hgm4si1AixCWlnvi5WUCq/+V+e8EoA2Apj9xJEt8zzXvtELlddFqkH2sfTFy9LIjGXKg== - dependencies: - cross-fetch "^3.1.5" +"@safe-global/safe-gateway-typescript-sdk@^3.11.0": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.11.0.tgz#e713dd98098e2fa126d3776c48625ab149863aef" + integrity sha512-jdNilX8US4KigO3GNCasZD121jLemKTmnT2GEH4LRNHecKAcB3bLcpFF/sYpozB0J+WiXKye75/g7xakNDf/hQ== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.0" From ceaacff11584fbcebecfbd2f449c6a72b073825c Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 7 Sep 2023 12:26:54 +0200 Subject: [PATCH 40/62] feat: add cached tracking --- .../PushNotificationsBanner/index.tsx | 10 +- .../__tests__/useNotificationTracking.test.ts | 106 ++++++++++++++++++ .../hooks/useNotificationTracking.ts | 77 +++++++++++++ src/pages/_app.tsx | 2 + src/service-workers/firebase-messaging-sw.ts | 22 +++- .../analytics/events/push-notifications.ts | 6 +- src/services/analytics/gtm.ts | 1 + src/services/analytics/types.ts | 1 + src/services/firebase/preferences.ts | 14 +-- src/services/firebase/tracking.ts | 71 ++++++++++++ 10 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts create mode 100644 src/components/settings/PushNotifications/hooks/useNotificationTracking.ts create mode 100644 src/services/firebase/tracking.ts diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index b12c94adf8..4caabc7420 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -30,8 +30,12 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } const [dismissedBannerPerChain = {}, setDismissedBannerPerChain] = useLocalStorage<{ [chainId: string]: boolean }>(DISMISS_NOTIFICATION_KEY) + + const hasAddedSafesOnChain = Object.values(addedSafes[safe.chainId] || {}).length > 0 const dismissedBanner = !!dismissedBannerPerChain[safe.chainId] + const shouldShowBanner = !dismissedBanner && hasAddedSafesOnChain + const { registerNotifications } = useNotificationRegistrations() const dismissBanner = useCallback(() => { @@ -45,7 +49,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } // Click outside to dismiss banner useEffect(() => { - if (dismissedBanner) { + if (!shouldShowBanner) { return } @@ -55,7 +59,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } return () => { document.removeEventListener('click', dismissBanner) } - }, [dismissBanner, dismissedBanner]) + }, [dismissBanner, shouldShowBanner]) const onEnableAll = async () => { trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL) @@ -72,7 +76,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } dismissBanner() } - if (dismissedBanner) { + if (!shouldShowBanner) { return children } diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts new file mode 100644 index 0000000000..03a80b1716 --- /dev/null +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts @@ -0,0 +1,106 @@ +import 'fake-indexeddb/auto' +import { entries, setMany } from 'idb-keyval' + +import * as tracking from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import { createNotificationTrackingIndexedDb } from '@/services/firebase/tracking' +import { WebhookType } from '@/services/firebase/webhooks' +import { renderHook, waitFor } from '@/tests/test-utils' +import { useNotificationTracking } from '../useNotificationTracking' + +jest.mock('@/services/analytics', () => ({ + trackEvent: jest.fn(), +})) + +describe('useNotificationTracking', () => { + beforeEach(() => { + // Reset indexedDB + indexedDB = new IDBFactory() + jest.clearAllMocks() + }) + + it('should track all cached events and clear the cache', async () => { + jest.spyOn(tracking, 'trackEvent') + + const cache = { + [`1:${WebhookType.INCOMING_ETHER}`]: { + shown: 1, + opened: 0, + }, + [`2:${WebhookType.OUTGOING_ETHER}`]: { + shown: 0, + opened: 1, + }, + [`3:${WebhookType.INCOMING_TOKEN}`]: { + shown: 1, + opened: 1, + }, + [`137:${WebhookType.OUTGOING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + } + + await setMany(Object.entries(cache), createNotificationTrackingIndexedDb()) + + renderHook(() => useNotificationTracking()) + + await waitFor(() => { + expect(tracking.trackEvent).toHaveBeenCalledTimes(4) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: WebhookType.INCOMING_ETHER, + chainId: '1', + }) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: WebhookType.OUTGOING_ETHER, + chainId: '2', + }) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: WebhookType.INCOMING_TOKEN, + chainId: '3', + }) + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: WebhookType.INCOMING_TOKEN, + chainId: '3', + }) + }) + + const _entries = await entries(createNotificationTrackingIndexedDb()) + expect(Object.fromEntries(_entries)).toStrictEqual({ + [`1:${WebhookType.INCOMING_ETHER}`]: { + shown: 0, + opened: 0, + }, + [`2:${WebhookType.OUTGOING_ETHER}`]: { + shown: 0, + opened: 0, + }, + [`3:${WebhookType.INCOMING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + [`137:${WebhookType.OUTGOING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + }) + }) + + it('should not track if no cache exists', async () => { + jest.spyOn(tracking, 'trackEvent') + + const _entries = await entries(createNotificationTrackingIndexedDb()) + expect(_entries).toStrictEqual([]) + + renderHook(() => useNotificationTracking()) + + expect(tracking.trackEvent).not.toHaveBeenCalled() + }) +}) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts new file mode 100644 index 0000000000..91a4f3dc64 --- /dev/null +++ b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts @@ -0,0 +1,77 @@ +import { keys as keysFromIndexedDb, update as updateIndexedDb } from 'idb-keyval' +import { useEffect, useMemo } from 'react' + +import { + _DEFAULT_WEBHOOK_TRACKING, + createNotificationTrackingIndexedDb, + parseNotificationTrackingKey, +} from '@/services/firebase/tracking' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import type { NotificationTracking, NotificationTrackingKey } from '@/services/firebase/tracking' +import type { WebhookType } from '@/services/firebase/webhooks' + +const trackNotificationEvents = ( + chainId: string, + type: WebhookType, + tracking: NotificationTracking[NotificationTrackingKey], +) => { + // Shown notifications + Array.from({ length: tracking.shown }).forEach(() => { + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: type, + chainId, + }) + }) + + // Opened notifications + Array.from({ length: tracking.opened }).forEach(() => { + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: type, + chainId, + }) + }) +} + +export const useNotificationTracking = (): void => { + // idb-keyval stores + const trackingStore = useMemo(() => { + if (typeof indexedDB !== 'undefined') { + return createNotificationTrackingIndexedDb() + } + }, []) + + useEffect(() => { + if (!trackingStore) { + return + } + + // Get all tracked webhooks + keysFromIndexedDb(trackingStore) + .then(async (trackedWebhooks) => { + // For each type, get the tracking data, track it and then clear the entry + const trackingPromises = trackedWebhooks + .map(async (trackedWebhook) => { + const { chainId, type } = parseNotificationTrackingKey(trackedWebhook) + + return updateIndexedDb( + trackedWebhook, + (tracking) => { + if (tracking) { + trackNotificationEvents(chainId, type, tracking) + } + + return _DEFAULT_WEBHOOK_TRACKING + }, + trackingStore, + ).catch(() => null) + }) + .filter((promise): promise is Promise => Boolean(promise)) + + await Promise.all(trackingPromises) + }) + .catch(() => null) + }, [trackingStore]) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d6612c4179..602e6f0da9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,6 +37,7 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' +import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -44,6 +45,7 @@ const InitApp = (): null => { setGatewayBaseUrl(GATEWAY_URL) useAdjustUrl() useGtm() + useNotificationTracking() useInitSession() useLoadableStores() useInitOnboard() diff --git a/src/service-workers/firebase-messaging-sw.ts b/src/service-workers/firebase-messaging-sw.ts index a7cbb508de..6f7bad4f05 100644 --- a/src/service-workers/firebase-messaging-sw.ts +++ b/src/service-workers/firebase-messaging-sw.ts @@ -3,12 +3,18 @@ /// import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' +import type { MessagePayload } from 'firebase/messaging/sw' import { initializeFirebase } from '@/services/firebase/app' import { shouldShowNotification, parseFirebaseNotification } from '@/services/firebase/notifications' +import { cacheNotificationTrackingProperty as cacheNotificationTracking } from '@/services/firebase/tracking' declare const self: ServiceWorkerGlobalScope +export type NotificationData = MessagePayload['data'] & { + link: string +} + export function firebaseMessagingSw() { const ICON_PATH = '/images/safe-logo-green.png' @@ -24,11 +30,9 @@ export function firebaseMessagingSw() { (event) => { event.notification.close() - const link = event.notification.tag + const { link }: NotificationData = event.notification.data - if (!link) { - return - } + cacheNotificationTracking('opened', event.notification.data) self.clients.openWindow(link) }, @@ -50,11 +54,19 @@ export function firebaseMessagingSw() { return } + const data: NotificationData = { + ...payload.data, + link: notification.link ?? self.location.origin, + } + + cacheNotificationTracking('shown', data) + self.registration.showNotification(notification.title, { icon: ICON_PATH, body: notification.body, image: notification.image, - tag: notification.link ?? self.location.origin, + // Used as type is any + data, }) }) } diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts index d4884d1523..d5731c519e 100644 --- a/src/services/analytics/events/push-notifications.ts +++ b/src/services/analytics/events/push-notifications.ts @@ -6,9 +6,9 @@ export const PUSH_NOTIFICATION_EVENTS = { action: 'Show notification', category, }, - // User clicked on notification - CLICK_NOTIFICATION: { - action: 'Click notification', + // User opened on notification + OPEN_NOTIFICATION: { + action: 'Open notification', category, }, // User granted notification permissions diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 2d376cff41..232a247766 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -104,6 +104,7 @@ export const gtmTrack = (eventData: AnalyticsEvent): void => { event: eventData.event || EventType.CLICK, eventCategory: eventData.category, eventAction: eventData.action, + chainId: eventData.chainId || commonEventParams.chainId, } if (eventData.event) { diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index 62e43d6d50..b2df3d6c6e 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -15,6 +15,7 @@ export type AnalyticsEvent = { category: string action: string label?: EventLabel + chainId?: string } export type SafeAppSDKEvent = { diff --git a/src/services/firebase/preferences.ts b/src/services/firebase/preferences.ts index 04f175f5c7..a2deaeb1e5 100644 --- a/src/services/firebase/preferences.ts +++ b/src/services/firebase/preferences.ts @@ -4,13 +4,6 @@ import { createStore as createIndexedDb } from 'idb-keyval' import type { WebhookType } from './webhooks' -export const createNotificationUuidIndexedDb = () => { - const DB_NAME = 'notifications-uuid-database' - const STORE_NAME = 'notifications-uuid-store' - - return createIndexedDb(DB_NAME, STORE_NAME) -} - export type SafeNotificationPrefsKey = `${string}:${string}` export type NotificationPreferences = { @@ -25,6 +18,13 @@ export const getSafeNotificationPrefsKey = (chainId: string, safeAddress: string return `${chainId}:${safeAddress}` } +export const createNotificationUuidIndexedDb = () => { + const DB_NAME = 'notifications-uuid-database' + const STORE_NAME = 'notifications-uuid-store' + + return createIndexedDb(DB_NAME, STORE_NAME) +} + export const createNotificationPrefsIndexedDb = () => { const DB_NAME = 'notifications-preferences-database' const STORE_NAME = 'notifications-preferences-store' diff --git a/src/services/firebase/tracking.ts b/src/services/firebase/tracking.ts new file mode 100644 index 0000000000..0a6a11c6fb --- /dev/null +++ b/src/services/firebase/tracking.ts @@ -0,0 +1,71 @@ +// Be careful what you import here as it will increase the service worker bundle size + +import { createStore as createIndexedDb, update as updateIndexedDb } from 'idb-keyval' + +import { isWebhookEvent, WebhookType } from './webhooks' +import type { NotificationData } from '@/service-workers/firebase-messaging-sw' + +export type NotificationTrackingKey = `${string}:${WebhookType}` + +export type NotificationTracking = { + [chainKey: NotificationTrackingKey]: { + shown: number + opened: number + } +} + +export const getNotificationTrackingKey = (chainId: string, type: WebhookType): NotificationTrackingKey => { + return `${chainId}:${type}` +} + +export const parseNotificationTrackingKey = (key: string): { chainId: string; type: WebhookType } => { + const [chainId, type] = key.split(':') + + if (!Object.keys(WebhookType).includes(type)) { + throw new Error(`Invalid notification tracking key: ${key}`) + } + + return { + chainId, + type: type as WebhookType, + } +} + +export const createNotificationTrackingIndexedDb = () => { + const DB_NAME = 'notifications-tracking-database' + const STORE_NAME = 'notifications-tracking-store' + + return createIndexedDb(DB_NAME, STORE_NAME) +} + +export const _DEFAULT_WEBHOOK_TRACKING: NotificationTracking[NotificationTrackingKey] = { + shown: 0, + opened: 0, +} + +export const cacheNotificationTrackingProperty = ( + property: keyof NotificationTracking[NotificationTrackingKey], + data: NotificationData, +) => { + if (!isWebhookEvent(data)) { + return + } + + const key = getNotificationTrackingKey(data.chainId, data.type) + const store = createNotificationTrackingIndexedDb() + + updateIndexedDb( + key, + (tracking) => { + if (tracking) { + return { + ...tracking, + [property]: (tracking[property] ?? 0) + 1, + } + } + + return _DEFAULT_WEBHOOK_TRACKING + }, + store, + ).catch(() => null) +} From 2350d58a108d423bc9695e2ed17d3fe4f14d51b7 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 7 Sep 2023 12:51:42 +0200 Subject: [PATCH 41/62] fix: test --- src/services/firebase/__tests__/notifications.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/firebase/__tests__/notifications.test.ts b/src/services/firebase/__tests__/notifications.test.ts index 1b220d63ce..d94733dd76 100644 --- a/src/services/firebase/__tests__/notifications.test.ts +++ b/src/services/firebase/__tests__/notifications.test.ts @@ -33,6 +33,10 @@ describe('parseWebhookNotification', () => { getBalancesMockSpy = jest.spyOn(sdk, 'getBalances') }) + afterAll(() => { + jest.restoreAllMocks() + }) + describe('should parse NEW_CONFIRMATION payloads', () => { const payload: NewConfirmationEvent = { type: WebhookType.NEW_CONFIRMATION, From e10df30a2df52a6feea006650c76ac390e8c31c3 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 7 Sep 2023 13:36:36 +0200 Subject: [PATCH 42/62] fix: mock response on Safe Apps share page --- .env.example | 18 +-- .../firebase/__tests__/notifications.test.ts | 4 - src/tests/pages/apps-share.test.tsx | 123 +++++++++++++----- 3 files changed, 89 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index f388430182..6642c2c52c 100644 --- a/.env.example +++ b/.env.example @@ -33,24 +33,10 @@ NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION= NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING= # Firebase Cloud Messaging -NEXT_PUBLIC_FIREBASE_API_KEY_PRODUCTION= -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_PRODUCTION= -NEXT_PUBLIC_FIREBASE_DATABASE_URL_PRODUCTION= -NEXT_PUBLIC_FIREBASE_PROJECT_ID_PRODUCTION= -NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_PRODUCTION= -NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_PRODUCTION= -NEXT_PUBLIC_FIREBASE_APP_ID_PRODUCTION= -NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_PRODUCTION= +NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION= NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION= -NEXT_PUBLIC_FIREBASE_API_KEY_STAGING= -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN_STAGING= -NEXT_PUBLIC_FIREBASE_DATABASE_URL_STAGING= -NEXT_PUBLIC_FIREBASE_PROJECT_ID_STAGING= -NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET_STAGING= -NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID_STAGING= -NEXT_PUBLIC_FIREBASE_APP_ID_STAGING= -NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID_STAGING= +NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING= NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= # Redefine diff --git a/src/services/firebase/__tests__/notifications.test.ts b/src/services/firebase/__tests__/notifications.test.ts index d94733dd76..1b220d63ce 100644 --- a/src/services/firebase/__tests__/notifications.test.ts +++ b/src/services/firebase/__tests__/notifications.test.ts @@ -33,10 +33,6 @@ describe('parseWebhookNotification', () => { getBalancesMockSpy = jest.spyOn(sdk, 'getBalances') }) - afterAll(() => { - jest.restoreAllMocks() - }) - describe('should parse NEW_CONFIRMATION payloads', () => { const payload: NewConfirmationEvent = { type: WebhookType.NEW_CONFIRMATION, diff --git a/src/tests/pages/apps-share.test.tsx b/src/tests/pages/apps-share.test.tsx index 598c6bb95b..383ca58183 100644 --- a/src/tests/pages/apps-share.test.tsx +++ b/src/tests/pages/apps-share.test.tsx @@ -4,16 +4,67 @@ import ShareSafeApp from '@/pages/share/safe-app' import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' import * as useWalletHook from '@/hooks/wallets/useWallet' import * as useOwnedSafesHook from '@/hooks/useOwnedSafes' +import * as manifest from '@/services/safe-apps/manifest' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' import crypto from 'crypto' import type { EIP1193Provider } from '@web3-onboard/core' -const FETCH_TIMEOUT = 5000 const TX_BUILDER = 'https://apps-portal.safe.global/tx-builder' describe('Share Safe App Page', () => { + let fetchSafeAppFromManifestSpy: jest.SpyInstance> + let getSafeAppsSpy: jest.SpyInstance> + beforeEach(() => { jest.restoreAllMocks() window.localStorage.clear() + + fetchSafeAppFromManifestSpy = jest.spyOn(manifest, 'fetchSafeAppFromManifest').mockResolvedValue({ + id: Math.random(), + url: TX_BUILDER, + name: 'Transaction Builder', + description: 'A Safe app to compose custom transactions', + accessControl: { type: sdk.SafeAppAccessPolicyTypes.NoRestrictions }, + tags: [], + features: [], + socialProfiles: [], + developerWebsite: '', + chainIds: ['1'], + iconUrl: `${TX_BUILDER}/tx-builder.png`, + safeAppsPermissions: [], + }) + + getSafeAppsSpy = jest.spyOn(sdk, 'getSafeApps').mockResolvedValue([ + { + id: 29, + url: TX_BUILDER, + name: 'Transaction Builder', + iconUrl: `${TX_BUILDER}/tx-builder.png`, + description: 'Compose custom contract interactions and batch them into a single transaction', + chainIds: ['1'], + provider: undefined, + accessControl: { + type: sdk.SafeAppAccessPolicyTypes.NoRestrictions, + }, + tags: ['dashboard-widgets', 'Infrastructure', 'transaction-builder'], + features: [sdk.SafeAppFeatures.BATCHED_TRANSACTIONS], + developerWebsite: 'https://safe.global', + socialProfiles: [ + { + platform: sdk.SafeAppSocialPlatforms.DISCORD, + url: 'https://chat.safe.global', + }, + { + platform: sdk.SafeAppSocialPlatforms.GITHUB, + url: 'https://github.com/safe-global', + }, + { + platform: sdk.SafeAppSocialPlatforms.TWITTER, + url: 'https://twitter.com/safe', + }, + ], + }, + ]) }) it('Should show the app name, description and URL', async () => { @@ -33,16 +84,16 @@ describe('Share Safe App Page', () => { }, }) - await waitFor( - () => { - expect(screen.getByText('Transaction Builder')).toBeInTheDocument() - expect( - screen.getByText('Compose custom contract interactions and batch them into a single transaction'), - ).toBeInTheDocument() - expect(screen.getByText(TX_BUILDER)).toBeInTheDocument() - }, - { timeout: FETCH_TIMEOUT }, - ) + await waitFor(() => { + expect(fetchSafeAppFromManifestSpy).toHaveBeenCalledWith(TX_BUILDER, '1') + expect(getSafeAppsSpy).toHaveBeenCalledWith('1', { url: TX_BUILDER }) + + expect(screen.getByText('Transaction Builder')).toBeInTheDocument() + expect( + screen.getByText('Compose custom contract interactions and batch them into a single transaction'), + ).toBeInTheDocument() + expect(screen.getByText(TX_BUILDER)).toBeInTheDocument() + }) }) it("Should suggest to connect a wallet when user hasn't connected one", async () => { @@ -62,12 +113,12 @@ describe('Share Safe App Page', () => { }, }) - await waitFor( - () => { - expect(screen.getByText('Connect wallet')).toBeInTheDocument() - }, - { timeout: FETCH_TIMEOUT }, - ) + await waitFor(() => { + expect(fetchSafeAppFromManifestSpy).toHaveBeenCalledWith(TX_BUILDER, '1') + expect(getSafeAppsSpy).toHaveBeenCalledWith('1', { url: TX_BUILDER }) + + expect(screen.getByText('Connect wallet')).toBeInTheDocument() + }) }) it('Should show a link to the demo on mainnet', async () => { @@ -87,12 +138,12 @@ describe('Share Safe App Page', () => { }, }) - await waitFor( - () => { - expect(screen.getByText('Try demo')).toBeInTheDocument() - }, - { timeout: FETCH_TIMEOUT }, - ) + await waitFor(() => { + expect(fetchSafeAppFromManifestSpy).toHaveBeenCalledWith(TX_BUILDER, '1') + expect(getSafeAppsSpy).toHaveBeenCalledWith('1', { url: TX_BUILDER }) + + expect(screen.getByText('Try demo')).toBeInTheDocument() + }) }) it('Should link to Safe Creation flow when the connected wallet has no owned Safes', async () => { @@ -108,7 +159,7 @@ describe('Share Safe App Page', () => { render(, { routerProps: { query: { - appUrl: 'https://apps-portal.safe.global/tx-builder/', + appUrl: TX_BUILDER, chain: 'rin', }, }, @@ -121,12 +172,12 @@ describe('Share Safe App Page', () => { }, }) - await waitFor( - () => { - expect(screen.getByText('Create new Safe Account')).toBeInTheDocument() - }, - { timeout: FETCH_TIMEOUT }, - ) + await waitFor(() => { + expect(fetchSafeAppFromManifestSpy).toHaveBeenCalledWith(TX_BUILDER, '4') + expect(getSafeAppsSpy).toHaveBeenCalledWith('4', { url: TX_BUILDER }) + + expect(screen.getByText('Create new Safe Account')).toBeInTheDocument() + }) }) it('Should show a select input with owned safes when the connected wallet owns Safes', async () => { @@ -159,11 +210,11 @@ describe('Share Safe App Page', () => { }, }) - await waitFor( - () => { - expect(screen.getByLabelText('Select a Safe Account')).toBeInTheDocument() - }, - { timeout: FETCH_TIMEOUT }, - ) + await waitFor(() => { + expect(fetchSafeAppFromManifestSpy).toHaveBeenCalledWith(TX_BUILDER, '1') + expect(getSafeAppsSpy).toHaveBeenCalledWith('1', { url: TX_BUILDER }) + + expect(screen.getByLabelText('Select a Safe Account')).toBeInTheDocument() + }) }) }) From 168886e0213778bb3558b858e887ee27d93736de Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 7 Sep 2023 16:33:02 +0200 Subject: [PATCH 43/62] revert: cached tracking --- .../__tests__/useNotificationTracking.test.ts | 106 ------------------ .../hooks/useNotificationTracking.ts | 77 ------------- src/pages/_app.tsx | 2 - src/service-workers/firebase-messaging-sw.ts | 22 +--- .../analytics/events/push-notifications.ts | 6 +- src/services/analytics/gtm.ts | 1 - src/services/analytics/types.ts | 1 - src/services/firebase/preferences.ts | 14 +-- src/services/firebase/tracking.ts | 71 ------------ 9 files changed, 15 insertions(+), 285 deletions(-) delete mode 100644 src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts delete mode 100644 src/components/settings/PushNotifications/hooks/useNotificationTracking.ts delete mode 100644 src/services/firebase/tracking.ts diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts deleted file mode 100644 index 03a80b1716..0000000000 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import 'fake-indexeddb/auto' -import { entries, setMany } from 'idb-keyval' - -import * as tracking from '@/services/analytics' -import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' -import { createNotificationTrackingIndexedDb } from '@/services/firebase/tracking' -import { WebhookType } from '@/services/firebase/webhooks' -import { renderHook, waitFor } from '@/tests/test-utils' -import { useNotificationTracking } from '../useNotificationTracking' - -jest.mock('@/services/analytics', () => ({ - trackEvent: jest.fn(), -})) - -describe('useNotificationTracking', () => { - beforeEach(() => { - // Reset indexedDB - indexedDB = new IDBFactory() - jest.clearAllMocks() - }) - - it('should track all cached events and clear the cache', async () => { - jest.spyOn(tracking, 'trackEvent') - - const cache = { - [`1:${WebhookType.INCOMING_ETHER}`]: { - shown: 1, - opened: 0, - }, - [`2:${WebhookType.OUTGOING_ETHER}`]: { - shown: 0, - opened: 1, - }, - [`3:${WebhookType.INCOMING_TOKEN}`]: { - shown: 1, - opened: 1, - }, - [`137:${WebhookType.OUTGOING_TOKEN}`]: { - shown: 0, - opened: 0, - }, - } - - await setMany(Object.entries(cache), createNotificationTrackingIndexedDb()) - - renderHook(() => useNotificationTracking()) - - await waitFor(() => { - expect(tracking.trackEvent).toHaveBeenCalledTimes(4) - - expect(tracking.trackEvent).toHaveBeenCalledWith({ - ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, - label: WebhookType.INCOMING_ETHER, - chainId: '1', - }) - - expect(tracking.trackEvent).toHaveBeenCalledWith({ - ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, - label: WebhookType.OUTGOING_ETHER, - chainId: '2', - }) - - expect(tracking.trackEvent).toHaveBeenCalledWith({ - ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, - label: WebhookType.INCOMING_TOKEN, - chainId: '3', - }) - expect(tracking.trackEvent).toHaveBeenCalledWith({ - ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, - label: WebhookType.INCOMING_TOKEN, - chainId: '3', - }) - }) - - const _entries = await entries(createNotificationTrackingIndexedDb()) - expect(Object.fromEntries(_entries)).toStrictEqual({ - [`1:${WebhookType.INCOMING_ETHER}`]: { - shown: 0, - opened: 0, - }, - [`2:${WebhookType.OUTGOING_ETHER}`]: { - shown: 0, - opened: 0, - }, - [`3:${WebhookType.INCOMING_TOKEN}`]: { - shown: 0, - opened: 0, - }, - [`137:${WebhookType.OUTGOING_TOKEN}`]: { - shown: 0, - opened: 0, - }, - }) - }) - - it('should not track if no cache exists', async () => { - jest.spyOn(tracking, 'trackEvent') - - const _entries = await entries(createNotificationTrackingIndexedDb()) - expect(_entries).toStrictEqual([]) - - renderHook(() => useNotificationTracking()) - - expect(tracking.trackEvent).not.toHaveBeenCalled() - }) -}) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts deleted file mode 100644 index 91a4f3dc64..0000000000 --- a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { keys as keysFromIndexedDb, update as updateIndexedDb } from 'idb-keyval' -import { useEffect, useMemo } from 'react' - -import { - _DEFAULT_WEBHOOK_TRACKING, - createNotificationTrackingIndexedDb, - parseNotificationTrackingKey, -} from '@/services/firebase/tracking' -import { trackEvent } from '@/services/analytics' -import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' -import type { NotificationTracking, NotificationTrackingKey } from '@/services/firebase/tracking' -import type { WebhookType } from '@/services/firebase/webhooks' - -const trackNotificationEvents = ( - chainId: string, - type: WebhookType, - tracking: NotificationTracking[NotificationTrackingKey], -) => { - // Shown notifications - Array.from({ length: tracking.shown }).forEach(() => { - trackEvent({ - ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, - label: type, - chainId, - }) - }) - - // Opened notifications - Array.from({ length: tracking.opened }).forEach(() => { - trackEvent({ - ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, - label: type, - chainId, - }) - }) -} - -export const useNotificationTracking = (): void => { - // idb-keyval stores - const trackingStore = useMemo(() => { - if (typeof indexedDB !== 'undefined') { - return createNotificationTrackingIndexedDb() - } - }, []) - - useEffect(() => { - if (!trackingStore) { - return - } - - // Get all tracked webhooks - keysFromIndexedDb(trackingStore) - .then(async (trackedWebhooks) => { - // For each type, get the tracking data, track it and then clear the entry - const trackingPromises = trackedWebhooks - .map(async (trackedWebhook) => { - const { chainId, type } = parseNotificationTrackingKey(trackedWebhook) - - return updateIndexedDb( - trackedWebhook, - (tracking) => { - if (tracking) { - trackNotificationEvents(chainId, type, tracking) - } - - return _DEFAULT_WEBHOOK_TRACKING - }, - trackingStore, - ).catch(() => null) - }) - .filter((promise): promise is Promise => Boolean(promise)) - - await Promise.all(trackingPromises) - }) - .catch(() => null) - }, [trackingStore]) -} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 602e6f0da9..d6612c4179 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,7 +37,6 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' -import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -45,7 +44,6 @@ const InitApp = (): null => { setGatewayBaseUrl(GATEWAY_URL) useAdjustUrl() useGtm() - useNotificationTracking() useInitSession() useLoadableStores() useInitOnboard() diff --git a/src/service-workers/firebase-messaging-sw.ts b/src/service-workers/firebase-messaging-sw.ts index 6f7bad4f05..a7cbb508de 100644 --- a/src/service-workers/firebase-messaging-sw.ts +++ b/src/service-workers/firebase-messaging-sw.ts @@ -3,18 +3,12 @@ /// import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' -import type { MessagePayload } from 'firebase/messaging/sw' import { initializeFirebase } from '@/services/firebase/app' import { shouldShowNotification, parseFirebaseNotification } from '@/services/firebase/notifications' -import { cacheNotificationTrackingProperty as cacheNotificationTracking } from '@/services/firebase/tracking' declare const self: ServiceWorkerGlobalScope -export type NotificationData = MessagePayload['data'] & { - link: string -} - export function firebaseMessagingSw() { const ICON_PATH = '/images/safe-logo-green.png' @@ -30,9 +24,11 @@ export function firebaseMessagingSw() { (event) => { event.notification.close() - const { link }: NotificationData = event.notification.data + const link = event.notification.tag - cacheNotificationTracking('opened', event.notification.data) + if (!link) { + return + } self.clients.openWindow(link) }, @@ -54,19 +50,11 @@ export function firebaseMessagingSw() { return } - const data: NotificationData = { - ...payload.data, - link: notification.link ?? self.location.origin, - } - - cacheNotificationTracking('shown', data) - self.registration.showNotification(notification.title, { icon: ICON_PATH, body: notification.body, image: notification.image, - // Used as type is any - data, + tag: notification.link ?? self.location.origin, }) }) } diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts index d5731c519e..d4884d1523 100644 --- a/src/services/analytics/events/push-notifications.ts +++ b/src/services/analytics/events/push-notifications.ts @@ -6,9 +6,9 @@ export const PUSH_NOTIFICATION_EVENTS = { action: 'Show notification', category, }, - // User opened on notification - OPEN_NOTIFICATION: { - action: 'Open notification', + // User clicked on notification + CLICK_NOTIFICATION: { + action: 'Click notification', category, }, // User granted notification permissions diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 232a247766..2d376cff41 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -104,7 +104,6 @@ export const gtmTrack = (eventData: AnalyticsEvent): void => { event: eventData.event || EventType.CLICK, eventCategory: eventData.category, eventAction: eventData.action, - chainId: eventData.chainId || commonEventParams.chainId, } if (eventData.event) { diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index b2df3d6c6e..62e43d6d50 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -15,7 +15,6 @@ export type AnalyticsEvent = { category: string action: string label?: EventLabel - chainId?: string } export type SafeAppSDKEvent = { diff --git a/src/services/firebase/preferences.ts b/src/services/firebase/preferences.ts index a2deaeb1e5..04f175f5c7 100644 --- a/src/services/firebase/preferences.ts +++ b/src/services/firebase/preferences.ts @@ -4,6 +4,13 @@ import { createStore as createIndexedDb } from 'idb-keyval' import type { WebhookType } from './webhooks' +export const createNotificationUuidIndexedDb = () => { + const DB_NAME = 'notifications-uuid-database' + const STORE_NAME = 'notifications-uuid-store' + + return createIndexedDb(DB_NAME, STORE_NAME) +} + export type SafeNotificationPrefsKey = `${string}:${string}` export type NotificationPreferences = { @@ -18,13 +25,6 @@ export const getSafeNotificationPrefsKey = (chainId: string, safeAddress: string return `${chainId}:${safeAddress}` } -export const createNotificationUuidIndexedDb = () => { - const DB_NAME = 'notifications-uuid-database' - const STORE_NAME = 'notifications-uuid-store' - - return createIndexedDb(DB_NAME, STORE_NAME) -} - export const createNotificationPrefsIndexedDb = () => { const DB_NAME = 'notifications-preferences-database' const STORE_NAME = 'notifications-preferences-store' diff --git a/src/services/firebase/tracking.ts b/src/services/firebase/tracking.ts deleted file mode 100644 index 0a6a11c6fb..0000000000 --- a/src/services/firebase/tracking.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Be careful what you import here as it will increase the service worker bundle size - -import { createStore as createIndexedDb, update as updateIndexedDb } from 'idb-keyval' - -import { isWebhookEvent, WebhookType } from './webhooks' -import type { NotificationData } from '@/service-workers/firebase-messaging-sw' - -export type NotificationTrackingKey = `${string}:${WebhookType}` - -export type NotificationTracking = { - [chainKey: NotificationTrackingKey]: { - shown: number - opened: number - } -} - -export const getNotificationTrackingKey = (chainId: string, type: WebhookType): NotificationTrackingKey => { - return `${chainId}:${type}` -} - -export const parseNotificationTrackingKey = (key: string): { chainId: string; type: WebhookType } => { - const [chainId, type] = key.split(':') - - if (!Object.keys(WebhookType).includes(type)) { - throw new Error(`Invalid notification tracking key: ${key}`) - } - - return { - chainId, - type: type as WebhookType, - } -} - -export const createNotificationTrackingIndexedDb = () => { - const DB_NAME = 'notifications-tracking-database' - const STORE_NAME = 'notifications-tracking-store' - - return createIndexedDb(DB_NAME, STORE_NAME) -} - -export const _DEFAULT_WEBHOOK_TRACKING: NotificationTracking[NotificationTrackingKey] = { - shown: 0, - opened: 0, -} - -export const cacheNotificationTrackingProperty = ( - property: keyof NotificationTracking[NotificationTrackingKey], - data: NotificationData, -) => { - if (!isWebhookEvent(data)) { - return - } - - const key = getNotificationTrackingKey(data.chainId, data.type) - const store = createNotificationTrackingIndexedDb() - - updateIndexedDb( - key, - (tracking) => { - if (tracking) { - return { - ...tracking, - [property]: (tracking[property] ?? 0) + 1, - } - } - - return _DEFAULT_WEBHOOK_TRACKING - }, - store, - ).catch(() => null) -} From bbe73f4277ad22d090b588b66f45f57e3d06d560 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 7 Sep 2023 17:01:08 +0200 Subject: [PATCH 44/62] refactor: restructure/rename + add error codes --- .../GlobalPushNotifications.tsx | 4 +- .../useNotificationPreferences.test.ts | 19 ++-- .../hooks/useNotificationPreferences.ts | 42 ++++----- .../hooks/useNotificationRegistrations.ts | 4 +- .../settings/PushNotifications/index.tsx | 2 +- .../settings/PushNotifications/logic.ts | 8 +- .../__tests__/notifications.test.ts | 80 ++++++++-------- .../firebase-messaging-sw.ts | 15 +-- .../firebase-messaging}/notifications.ts | 91 +++++++++---------- .../firebase-messaging/webhook-types.ts} | 0 src/service-workers/index.ts | 2 +- src/services/exceptions/ErrorCodes.ts | 3 + .../app.ts => push-notifications/firebase.ts} | 2 +- .../preferences.ts | 26 +++--- 14 files changed, 153 insertions(+), 145 deletions(-) rename src/{services/firebase => service-workers/firebase-messaging}/__tests__/notifications.test.ts (87%) rename src/service-workers/{ => firebase-messaging}/firebase-messaging-sw.ts (68%) rename src/{services/firebase => service-workers/firebase-messaging}/notifications.ts (80%) rename src/{services/firebase/webhooks.ts => service-workers/firebase-messaging/webhook-types.ts} (100%) rename src/services/{firebase/app.ts => push-notifications/firebase.ts} (96%) rename src/services/{firebase => push-notifications}/preferences.ts (56%) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index f531e78610..4b962f011d 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -26,7 +26,7 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif import { requestNotificationPermission } from './logic' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' -import type { NotificationPreferences } from '@/services/firebase/preferences' +import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' import CheckWallet from '@/components/common/CheckWallet' import css from './styles.module.css' @@ -42,7 +42,7 @@ export const transformAddedSafes = (addedSafes: AddedSafesState): NotifiableSafe } // Convert data structure of currently notified Safes -const transformCurrentSubscribedSafes = (allPreferences?: NotificationPreferences): NotifiableSafes | undefined => { +const transformCurrentSubscribedSafes = (allPreferences?: PushNotificationPreferences): NotifiableSafes | undefined => { if (!allPreferences) { return } diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index 51de7ad865..160e329784 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -3,14 +3,17 @@ import { set, setMany } from 'idb-keyval' import { renderHook, waitFor } from '@/tests/test-utils' import { hexZeroPad } from 'ethers/lib/utils' -import { createNotificationUuidIndexedDb, createNotificationPrefsIndexedDb } from '@/services/firebase/preferences' +import { + createPushNotificationUuidIndexedDb, + createPushNotificationPrefsIndexedDb, +} from '@/services/push-notifications/preferences' import { useNotificationPreferences, _DEFAULT_NOTIFICATION_PREFERENCES, _setPreferences, _setUuid, } from '../useNotificationPreferences' -import { WebhookType } from '@/services/firebase/webhooks' +import { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' Object.defineProperty(globalThis, 'crypto', { value: { @@ -40,7 +43,7 @@ describe('useNotificationPreferences', () => { it('return uuid if it exists', async () => { const uuid = 'test-uuid' - await set('uuid', uuid, createNotificationUuidIndexedDb()) + await set('uuid', uuid, createPushNotificationUuidIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -67,7 +70,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -88,7 +91,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -146,7 +149,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -194,7 +197,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) @@ -238,7 +241,7 @@ describe('useNotificationPreferences', () => { }, } - await setMany(Object.entries(preferences), createNotificationPrefsIndexedDb()) + await setMany(Object.entries(preferences), createPushNotificationPrefsIndexedDb()) const { result } = renderHook(() => useNotificationPreferences()) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index 115be073bb..f55625236e 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -8,17 +8,17 @@ import { } from 'idb-keyval' import { useCallback, useEffect, useMemo } from 'react' -import { WebhookType } from '@/services/firebase/webhooks' +import { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' import ExternalStore from '@/services/ExternalStore' import { - createNotificationPrefsIndexedDb, - createNotificationUuidIndexedDb, - getSafeNotificationPrefsKey, -} from '@/services/firebase/preferences' -import type { NotificationPreferences, SafeNotificationPrefsKey } from '@/services/firebase/preferences' + createPushNotificationPrefsIndexedDb, + createPushNotificationUuidIndexedDb, + getPushNotificationPrefsKey, +} from '@/services/push-notifications/preferences' +import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' -export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNotificationPrefsKey]['preferences'] = { +export const _DEFAULT_NOTIFICATION_PREFERENCES: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'] = { [WebhookType.NEW_CONFIRMATION]: true, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, @@ -33,7 +33,7 @@ export const _DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences[SafeNoti // ExternalStores are used to keep indexedDB state synced across hook instances const { useStore: useUuid, setStore: setUuid } = new ExternalStore() -const { useStore: usePreferences, setStore: setPreferences } = new ExternalStore() +const { useStore: usePreferences, setStore: setPreferences } = new ExternalStore() // Used for testing export const _setUuid = setUuid @@ -41,12 +41,12 @@ export const _setPreferences = setPreferences export const useNotificationPreferences = (): { uuid: string | undefined - getAllPreferences: () => NotificationPreferences | undefined + getAllPreferences: () => PushNotificationPreferences | undefined getPreferences: (chainId: string, safeAddress: string) => typeof _DEFAULT_NOTIFICATION_PREFERENCES | undefined updatePreferences: ( chainId: string, safeAddress: string, - preferences: NotificationPreferences[SafeNotificationPrefsKey]['preferences'], + preferences: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'], ) => void _createPreferences: (safesToRegister: NotifiableSafes) => void _deletePreferences: (safesToUnregister: NotifiableSafes) => void @@ -58,7 +58,7 @@ export const useNotificationPreferences = (): { // Getters const getPreferences = (chainId: string, safeAddress: string) => { - const key = getSafeNotificationPrefsKey(chainId, safeAddress) + const key = getPushNotificationPrefsKey(chainId, safeAddress) return preferences?.[key]?.preferences } @@ -69,13 +69,13 @@ export const useNotificationPreferences = (): { // idb-keyval stores const uuidStore = useMemo(() => { if (typeof indexedDB !== 'undefined') { - return createNotificationUuidIndexedDb() + return createPushNotificationUuidIndexedDb() } }, []) const preferencesStore = useMemo(() => { if (typeof indexedDB !== 'undefined') { - return createNotificationPrefsIndexedDb() + return createPushNotificationPrefsIndexedDb() } }, []) @@ -115,7 +115,7 @@ export const useNotificationPreferences = (): { return } - getEntriesFromIndexedDb( + getEntriesFromIndexedDb( preferencesStore, ) .then((preferencesEntries) => { @@ -137,10 +137,10 @@ export const useNotificationPreferences = (): { const defaultPreferencesEntries = Object.entries(safesToRegister).flatMap(([chainId, safeAddresses]) => { return safeAddresses.map( - (safeAddress): [SafeNotificationPrefsKey, NotificationPreferences[SafeNotificationPrefsKey]] => { - const key = getSafeNotificationPrefsKey(chainId, safeAddress) + (safeAddress): [PushNotificationPrefsKey, PushNotificationPreferences[PushNotificationPrefsKey]] => { + const key = getPushNotificationPrefsKey(chainId, safeAddress) - const defaultPreferences: NotificationPreferences[SafeNotificationPrefsKey] = { + const defaultPreferences: PushNotificationPreferences[PushNotificationPrefsKey] = { chainId, safeAddress, preferences: _DEFAULT_NOTIFICATION_PREFERENCES, @@ -160,15 +160,15 @@ export const useNotificationPreferences = (): { const updatePreferences = ( chainId: string, safeAddress: string, - preferences: NotificationPreferences[SafeNotificationPrefsKey]['preferences'], + preferences: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'], ) => { if (!preferencesStore) { return } - const key = getSafeNotificationPrefsKey(chainId, safeAddress) + const key = getPushNotificationPrefsKey(chainId, safeAddress) - const newPreferences: NotificationPreferences[SafeNotificationPrefsKey] = { + const newPreferences: PushNotificationPreferences[PushNotificationPrefsKey] = { safeAddress, chainId, preferences, @@ -186,7 +186,7 @@ export const useNotificationPreferences = (): { } const keysToDelete = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { - return safeAddresses.map((safeAddress) => getSafeNotificationPrefsKey(chainId, safeAddress)) + return safeAddresses.map((safeAddress) => getPushNotificationPrefsKey(chainId, safeAddress)) }) deleteManyFromIndexedDb(keysToDelete, preferencesStore) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index ef41029fcf..ac9a956979 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -7,6 +7,8 @@ import { useNotificationPreferences } from './useNotificationPreferences' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { getRegisterDevicePayload } from '../logic' +import { logError } from '@/services/exceptions' +import ErrorCodes from '@/services/exceptions/ErrorCodes' import type { NotifiableSafes } from '../logic' const registrationFlow = async (registrationFn: Promise, callback: () => void) => { @@ -19,7 +21,7 @@ const registrationFlow = async (registrationFn: Promise, callback: () => v // @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) + logError(ErrorCodes._633, e) } if (success) { diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index 9a795ca280..9523e8b260 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -15,7 +15,7 @@ import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' import EthHashInfo from '@/components/common/EthHashInfo' -import { WebhookType } from '@/services/firebase/webhooks' +import { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' import { useNotificationRegistrations } from './hooks/useNotificationRegistrations' import { useNotificationPreferences } from './hooks/useNotificationPreferences' import { GlobalPushNotifications } from './GlobalPushNotifications' diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index ec8fb31803..9f2cf9cba6 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -4,10 +4,12 @@ import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk' import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk' import type { Web3Provider } from '@ethersproject/providers' -import { FIREBASE_VAPID_KEY, initializeFirebase } from '@/services/firebase/app' +import { FIREBASE_VAPID_KEY, initializeFirebaseApp } from '@/services/push-notifications/firebase' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' +import { logError } from '@/services/exceptions' +import ErrorCodes from '@/services/exceptions/ErrorCodes' type WithRequired = T & { [P in K]-?: T[P] } @@ -24,7 +26,7 @@ export const requestNotificationPermission = async (): Promise => { try { permission = await Notification.requestPermission() } catch (e) { - console.error('Error requesting notification permission', e) + logError(ErrorCodes._400, e) } const isGranted = permission === 'granted' @@ -76,7 +78,7 @@ export const getRegisterDevicePayload = async ({ const [serviceWorkerRegistration] = await navigator.serviceWorker.getRegistrations() // Get Firebase token - const app = initializeFirebase() + const app = initializeFirebaseApp() const messaging = getMessaging(app) const token = await getToken(messaging, { diff --git a/src/services/firebase/__tests__/notifications.test.ts b/src/service-workers/firebase-messaging/__tests__/notifications.test.ts similarity index 87% rename from src/services/firebase/__tests__/notifications.test.ts rename to src/service-workers/firebase-messaging/__tests__/notifications.test.ts index 1b220d63ce..6c6f622da1 100644 --- a/src/services/firebase/__tests__/notifications.test.ts +++ b/src/service-workers/firebase-messaging/__tests__/notifications.test.ts @@ -1,8 +1,8 @@ import { hexZeroPad } from 'ethers/lib/utils' import * as sdk from '@safe-global/safe-gateway-typescript-sdk' -import { _parseWebhookNotification } from '../notifications' -import { WebhookType } from '../webhooks' +import { _parseServiceWorkerWebhookPushNotification } from '../notifications' +import { WebhookType } from '../webhook-types' import type { ConfirmationRequestEvent, ExecutedMultisigTransactionEvent, @@ -14,7 +14,7 @@ import type { OutgoingTokenEvent, PendingMultisigTransactionEvent, SafeCreatedEvent, -} from '../webhooks' +} from '../webhook-types' jest.mock('@safe-global/safe-gateway-typescript-sdk') @@ -24,7 +24,7 @@ Object.defineProperty(self, 'location', { }, }) -describe('parseWebhookNotification', () => { +describe('parseWebhookPushNotification', () => { let getChainsConfigSpy: jest.SpyInstance> let getBalancesMockSpy: jest.SpyInstance> @@ -47,7 +47,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Transaction confirmation', @@ -59,7 +59,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Transaction confirmation', @@ -84,7 +84,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification({ + const notification = await _parseServiceWorkerWebhookPushNotification({ ...payload, failed: 'false', }) @@ -99,7 +99,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification({ + const notification = await _parseServiceWorkerWebhookPushNotification({ ...payload, failed: 'false', }) @@ -118,7 +118,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification({ + const notification = await _parseServiceWorkerWebhookPushNotification({ ...payload, failed: 'true', }) @@ -133,7 +133,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification({ + const notification = await _parseServiceWorkerWebhookPushNotification({ ...payload, failed: 'true', }) @@ -160,7 +160,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Pending transaction', @@ -172,7 +172,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Pending transaction', @@ -202,7 +202,7 @@ describe('parseWebhookNotification', () => { } as sdk.ChainInfo, ], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Matic received', @@ -214,7 +214,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Ether received', @@ -244,7 +244,7 @@ describe('parseWebhookNotification', () => { } as sdk.ChainInfo, ], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Matic sent', @@ -256,7 +256,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Ether sent', @@ -297,7 +297,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Fake received', @@ -321,7 +321,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Fake received', @@ -345,7 +345,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Fake received', @@ -367,7 +367,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Fake received', @@ -382,7 +382,7 @@ describe('parseWebhookNotification', () => { }) getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Token received', @@ -395,7 +395,7 @@ describe('parseWebhookNotification', () => { }) getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Token received', @@ -408,7 +408,7 @@ describe('parseWebhookNotification', () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Token received', @@ -419,7 +419,7 @@ describe('parseWebhookNotification', () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Token received', @@ -460,7 +460,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Fake sent', @@ -484,7 +484,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Fake sent', @@ -501,7 +501,7 @@ describe('parseWebhookNotification', () => { items: [] as sdk.SafeBalanceResponse['items'], // Transaction sent all of the tokens } as sdk.SafeBalanceResponse) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Token sent', @@ -525,7 +525,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Fake sent', @@ -549,7 +549,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Fake sent', @@ -571,7 +571,7 @@ describe('parseWebhookNotification', () => { ], } as sdk.SafeBalanceResponse) - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Fake sent', @@ -585,7 +585,7 @@ describe('parseWebhookNotification', () => { }) getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Token sent', @@ -598,7 +598,7 @@ describe('parseWebhookNotification', () => { }) getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Token sent', @@ -611,7 +611,7 @@ describe('parseWebhookNotification', () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Token sent', @@ -622,7 +622,7 @@ describe('parseWebhookNotification', () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens - const erc20Notification = await _parseWebhookNotification(erc20Payload) + const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) expect(erc20Notification).toEqual({ title: 'Token sent', @@ -646,7 +646,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Module transaction', @@ -658,7 +658,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Module transaction', @@ -681,7 +681,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Confirmation request', @@ -693,7 +693,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toEqual({ title: 'Confirmation request', @@ -716,7 +716,7 @@ describe('parseWebhookNotification', () => { results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], }) - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toBe(undefined) }) @@ -724,7 +724,7 @@ describe('parseWebhookNotification', () => { it('without chain info', async () => { getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - const notification = await _parseWebhookNotification(payload) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) expect(notification).toBe(undefined) }) diff --git a/src/service-workers/firebase-messaging-sw.ts b/src/service-workers/firebase-messaging/firebase-messaging-sw.ts similarity index 68% rename from src/service-workers/firebase-messaging-sw.ts rename to src/service-workers/firebase-messaging/firebase-messaging-sw.ts index a7cbb508de..6c0d6e8fe5 100644 --- a/src/service-workers/firebase-messaging-sw.ts +++ b/src/service-workers/firebase-messaging/firebase-messaging-sw.ts @@ -4,15 +4,18 @@ import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' -import { initializeFirebase } from '@/services/firebase/app' -import { shouldShowNotification, parseFirebaseNotification } from '@/services/firebase/notifications' +import { initializeFirebaseApp } from '@/services/push-notifications/firebase' +import { + shouldShowServiceWorkerPushNotification, + parseServiceWorkerPushNotification, +} from '@/service-workers/firebase-messaging/notifications' declare const self: ServiceWorkerGlobalScope export function firebaseMessagingSw() { const ICON_PATH = '/images/safe-logo-green.png' - const app = initializeFirebase() + const app = initializeFirebaseApp() if (!app) { return @@ -38,19 +41,19 @@ export function firebaseMessagingSw() { const messaging = getMessaging(app) onBackgroundMessage(messaging, async (payload) => { - const shouldShow = await shouldShowNotification(payload) + const shouldShow = await shouldShowServiceWorkerPushNotification(payload) if (!shouldShow) { return } - const notification = await parseFirebaseNotification(payload) + const notification = await parseServiceWorkerPushNotification(payload) if (!notification) { return } - self.registration.showNotification(notification.title, { + self.registration.showNotification(notification.title || '', { icon: ICON_PATH, body: notification.body, image: notification.image, diff --git a/src/services/firebase/notifications.ts b/src/service-workers/firebase-messaging/notifications.ts similarity index 80% rename from src/services/firebase/notifications.ts rename to src/service-workers/firebase-messaging/notifications.ts index 3352dc94a6..aca25d694a 100644 --- a/src/services/firebase/notifications.ts +++ b/src/service-workers/firebase-messaging/notifications.ts @@ -3,37 +3,34 @@ import { get as getFromIndexedDb } from 'idb-keyval' import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable import { getChainsConfig, getBalances, setBaseUrl } from '@safe-global/safe-gateway-typescript-sdk' +import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports -import { isWebhookEvent, WebhookType } from './webhooks' -import { getSafeNotificationPrefsKey, createNotificationPrefsIndexedDb } from './preferences' -import { FIREBASE_IS_PRODUCTION } from './app' -import type { WebhookEvent } from './webhooks' -import type { NotificationPreferences, SafeNotificationPrefsKey } from './preferences' - -const shortenAddress = (address: string, length = 4): string => { - if (!address) { - return '' - } - - return `${address.slice(0, length + 2)}...${address.slice(-length)}` -} - -export const shouldShowNotification = async (payload: MessagePayload): Promise => { +import { isWebhookEvent, WebhookType } from './webhook-types' +import { + getPushNotificationPrefsKey, + createPushNotificationPrefsIndexedDb, +} from '@/services/push-notifications/preferences' +import { FIREBASE_IS_PRODUCTION } from '@/services/push-notifications/firebase' +import type { WebhookEvent } from './webhook-types' +import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' + +export const shouldShowServiceWorkerPushNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { return true } const { chainId, address, type } = payload.data - const key = getSafeNotificationPrefsKey(chainId, address) - const store = createNotificationPrefsIndexedDb() + const key = getPushNotificationPrefsKey(chainId, address) + const store = createPushNotificationPrefsIndexedDb() - const preferencesStore = await getFromIndexedDb(key, store).catch( - () => null, - ) + const preferencesStore = await getFromIndexedDb( + key, + store, + ).catch(() => null) if (!preferencesStore) { return false @@ -69,9 +66,14 @@ const getTokenInfo = async ( name: 'Token', } - const tokenInfo = await getBalances(chainId, safeAddress, DEFAULT_CURRENCY) - .then(({ items }) => items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo) - .catch(() => null) + let tokenInfo: TokenInfo | undefined + + try { + const balances = await getBalances(chainId, safeAddress, DEFAULT_CURRENCY) + tokenInfo = balances.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo + } catch { + // Swallow error + } if (!tokenInfo) { return DEFAULT_INFO @@ -88,6 +90,14 @@ const getTokenInfo = async ( } } +const shortenAddress = (address: string, length = 4): string => { + if (!address) { + return '' + } + + return `${address.slice(0, length + 2)}...${address.slice(-length)}` +} + const getLink = (data: WebhookEvent, shortName?: string) => { const URL = self.location.origin @@ -106,13 +116,13 @@ const getLink = (data: WebhookEvent, shortName?: string) => { return withRoute(AppRoutes.transactions.history) } -type NotificationsMap = { +type PushNotificationsMap = { [P in T['type']]: ( data: Extract, ) => Promise<{ title: string; body: string }> | { title: string; body: string } | null } -export const _parseWebhookNotification = async ( +export const _parseServiceWorkerWebhookPushNotification = async ( data: WebhookEvent, ): Promise<{ title: string; body: string; link: string } | undefined> => { const chain = await getChain(data.chainId) @@ -122,7 +132,7 @@ export const _parseWebhookNotification = async ( const currencySymbol = chain?.nativeCurrency?.symbol ?? 'ETH' const currencyName = chain?.nativeCurrency?.name ?? 'Ether' - const Notifications: NotificationsMap = { + const Notifications: PushNotificationsMap = { [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash }) => { return { title: 'Transaction confirmation', @@ -217,29 +227,14 @@ export const _parseWebhookNotification = async ( } } -export const parseFirebaseNotification = async ( +export const parseServiceWorkerPushNotification = async ( payload: MessagePayload, -): Promise<({ title: string; link?: string } & NotificationOptions) | undefined> => { - // Transaction Service-dispatched notification - if (isWebhookEvent(payload.data)) { - const webhookNotification = await _parseWebhookNotification(payload.data) - - if (webhookNotification) { - return { - title: webhookNotification.title, - body: webhookNotification.body, - link: webhookNotification.link, - } - } +): Promise<({ title?: string; link?: string } & NotificationOptions) | undefined> => { + // Manually dispatched notifications from the Firebase admin panel; displayed as is + if (!isWebhookEvent(payload.data)) { + return payload.notification } - // Manually dispatched notifications from the Firebase admin panel - // Displayed as is - if (payload.notification) { - return { - title: payload.notification.title || '', - body: payload.notification.body, - image: payload.notification.image, - } - } + // Transaction Service-dispatched notification + return _parseServiceWorkerWebhookPushNotification(payload.data) } diff --git a/src/services/firebase/webhooks.ts b/src/service-workers/firebase-messaging/webhook-types.ts similarity index 100% rename from src/services/firebase/webhooks.ts rename to src/service-workers/firebase-messaging/webhook-types.ts diff --git a/src/service-workers/index.ts b/src/service-workers/index.ts index 4a9cf17c76..0ed829a135 100644 --- a/src/service-workers/index.ts +++ b/src/service-workers/index.ts @@ -2,6 +2,6 @@ /// -import { firebaseMessagingSw } from './firebase-messaging-sw' +import { firebaseMessagingSw } from './firebase-messaging/firebase-messaging-sw' firebaseMessagingSw() diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index ba7ac87a56..a45f9ab9c9 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -17,6 +17,8 @@ enum ErrorCodes { _302 = '302: Error connecting to the wallet', _303 = '303: Error creating pairing session', + _400 = '400: Error requesting browser notification permissions', + _600 = '600: Error fetching Safe info', _601 = '601: Error fetching balances', _602 = '602: Error fetching history txs', @@ -35,6 +37,7 @@ enum ErrorCodes { _630 = '630: Error fetching remaining hourly relays', _631 = '631: Transaction failed to be relayed', _632 = '632: Error fetching relay task status', + _633 = '633: Notification (un-)registration failed', _700 = '700: Failed to read from local/session storage', _701 = '701: Failed to write to local/session storage', diff --git a/src/services/firebase/app.ts b/src/services/push-notifications/firebase.ts similarity index 96% rename from src/services/firebase/app.ts rename to src/services/push-notifications/firebase.ts index 452d8323bf..909fa8209d 100644 --- a/src/services/firebase/app.ts +++ b/src/services/push-notifications/firebase.ts @@ -19,7 +19,7 @@ export const FIREBASE_OPTIONS: FirebaseOptions = (() => { } })() -export const initializeFirebase = () => { +export const initializeFirebaseApp = () => { const hasFirebaseOptions = Object.values(FIREBASE_OPTIONS).every(Boolean) if (!hasFirebaseOptions) { diff --git a/src/services/firebase/preferences.ts b/src/services/push-notifications/preferences.ts similarity index 56% rename from src/services/firebase/preferences.ts rename to src/services/push-notifications/preferences.ts index 04f175f5c7..fbc96cb23a 100644 --- a/src/services/firebase/preferences.ts +++ b/src/services/push-notifications/preferences.ts @@ -2,30 +2,30 @@ import { createStore as createIndexedDb } from 'idb-keyval' -import type { WebhookType } from './webhooks' +import type { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' -export const createNotificationUuidIndexedDb = () => { - const DB_NAME = 'notifications-uuid-database' - const STORE_NAME = 'notifications-uuid-store' - - return createIndexedDb(DB_NAME, STORE_NAME) -} - -export type SafeNotificationPrefsKey = `${string}:${string}` +export type PushNotificationPrefsKey = `${string}:${string}` -export type NotificationPreferences = { - [safeKey: SafeNotificationPrefsKey]: { +export type PushNotificationPreferences = { + [safeKey: PushNotificationPrefsKey]: { chainId: string safeAddress: string preferences: { [key in WebhookType]: boolean } } } -export const getSafeNotificationPrefsKey = (chainId: string, safeAddress: string): SafeNotificationPrefsKey => { +export const getPushNotificationPrefsKey = (chainId: string, safeAddress: string): PushNotificationPrefsKey => { return `${chainId}:${safeAddress}` } -export const createNotificationPrefsIndexedDb = () => { +export const createPushNotificationUuidIndexedDb = () => { + const DB_NAME = 'notifications-uuid-database' + const STORE_NAME = 'notifications-uuid-store' + + return createIndexedDb(DB_NAME, STORE_NAME) +} + +export const createPushNotificationPrefsIndexedDb = () => { const DB_NAME = 'notifications-preferences-database' const STORE_NAME = 'notifications-preferences-store' From a115c14973ce9cf0f7b6af14d7ae633ef6af035d Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 8 Sep 2023 12:54:06 +0200 Subject: [PATCH 45/62] fix: address review comments --- .../GlobalPushNotifications.tsx | 12 +- .../settings/PushNotifications/index.tsx | 2 +- .../firebase-messaging/notification-mapper.ts | 161 ++++++++++++++++ .../firebase-messaging/notifications.ts | 176 ++---------------- 4 files changed, 185 insertions(+), 166 deletions(-) create mode 100644 src/service-workers/firebase-messaging/notification-mapper.ts diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 4b962f011d..695c84b939 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -299,13 +299,14 @@ export const GlobalPushNotifications = (): ReactElement | null => { - My Safes ({totalNotifiableSafes}) + My Safes Accounts ({totalNotifiableSafes})
{totalSignaturesRequired > 0 && ( - - We'll ask you to verify with your signature {totalSignaturesRequired} times + + We'll ask you to verify your ownership of {totalSignaturesRequired} Safe Account + {totalSignaturesRequired > 1 ? 's' : ''} with your signature )} @@ -358,7 +359,10 @@ export const GlobalPushNotifications = (): ReactElement | null => { - + diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index 9523e8b260..df3a9605aa 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -124,7 +124,7 @@ export const PushNotifications = (): ReactElement => { - Want to setup notifications for different or all Safes? You can do so in your{' '} + Want to setup notifications for different or all Safe Accounts? You can do so in your{' '} global preferences diff --git a/src/service-workers/firebase-messaging/notification-mapper.ts b/src/service-workers/firebase-messaging/notification-mapper.ts new file mode 100644 index 0000000000..ad392908fe --- /dev/null +++ b/src/service-workers/firebase-messaging/notification-mapper.ts @@ -0,0 +1,161 @@ +// Be careful what you import here as it will increase the service worker bundle size + +import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable +import { getBalances } from '@safe-global/safe-gateway-typescript-sdk' +import type { ChainInfo, TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' + +import { WebhookType } from './webhook-types' +import type { WebhookEvent } from './webhook-types' + +type PushNotificationsMap = { + [P in T['type']]: ( + data: Extract, + chain?: ChainInfo, + ) => Promise<{ title: string; body: string }> | { title: string; body: string } | null +} + +const getChainName = (chainId: string, chain?: ChainInfo): string => { + return chain?.chainName ?? `chain ${chainId}` +} + +const getCurrencyName = (chain?: ChainInfo): string => { + return chain?.nativeCurrency?.name ?? 'Ether' +} + +const getCurrencySymbol = (chain?: ChainInfo): string => { + return chain?.nativeCurrency?.symbol ?? 'ETH' +} + +const getTokenInfo = async ( + chainId: string, + safeAddress: string, + tokenAddress: string, + tokenValue?: string, +): Promise<{ symbol: string; value: string; name: string }> => { + const DEFAULT_CURRENCY = 'USD' + + const DEFAULT_INFO = { + symbol: 'tokens', + value: 'some', + name: 'Token', + } + + let tokenInfo: TokenInfo | undefined + + try { + const balances = await getBalances(chainId, safeAddress, DEFAULT_CURRENCY) + tokenInfo = balances.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo + } catch { + // Swallow error + } + + if (!tokenInfo) { + return DEFAULT_INFO + } + + const symbol = tokenInfo?.symbol ?? DEFAULT_INFO.symbol + const value = tokenValue && tokenInfo ? formatUnits(tokenValue, tokenInfo.decimals).toString() : DEFAULT_INFO.value + const name = tokenInfo?.name ?? DEFAULT_INFO.name + + return { + symbol, + value, + name, + } +} + +const shortenAddress = (address: string, length = 4): string => { + if (!address) { + return '' + } + + return `${address.slice(0, length + 2)}...${address.slice(-length)}` +} + +export const Notifications: PushNotificationsMap = { + [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash, chainId }, chain) => { + return { + title: 'Transaction confirmation', + body: `Safe ${shortenAddress(address)} on ${getChainName( + chainId, + chain, + )} has a new confirmation from ${shortenAddress(owner)} on transaction ${shortenAddress(safeTxHash)}.`, + } + }, + [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash, chainId }, chain) => { + const didFail = failed === 'true' + return { + title: `Transaction ${didFail ? 'failed' : 'executed'}`, + body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} ${ + didFail ? 'failed to execute' : 'executed' + } transaction ${shortenAddress(txHash)}.`, + } + }, + [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash, chainId }, chain) => { + return { + title: 'Pending transaction', + body: `Safe ${shortenAddress(address)} on ${getChainName( + chainId, + chain, + )} has a pending transaction ${shortenAddress(safeTxHash)}.`, + } + }, + [WebhookType.INCOMING_ETHER]: ({ address, txHash, value, chainId }, chain) => { + return { + title: `${getCurrencyName(chain)} received`, + body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} received ${formatUnits( + value, + chain?.nativeCurrency?.decimals, + ).toString()} ${getCurrencySymbol(chain)} in transaction ${shortenAddress(txHash)}.`, + } + }, + [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value, chainId }, chain) => { + return { + title: `${getCurrencyName(chain)} sent`, + body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} sent ${formatUnits( + value, + chain?.nativeCurrency?.decimals, + ).toString()} ${getCurrencySymbol(chain)} in transaction ${shortenAddress(txHash)}.`, + } + }, + [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value, chainId }, chain) => { + const token = await getTokenInfo(chainId, address, tokenAddress, value) + return { + title: `${token.name} received`, + body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} received ${token.value} ${ + token.symbol + } in transaction ${shortenAddress(txHash)}.`, + } + }, + [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value, chainId }, chain) => { + const token = await getTokenInfo(chainId, address, tokenAddress, value) + return { + title: `${token.name} sent`, + body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} sent ${token.value} ${ + token.symbol + } in transaction ${shortenAddress(txHash)}.`, + } + }, + [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash, chainId }, chain) => { + return { + title: 'Module transaction', + body: `Safe ${shortenAddress(address)} on ${getChainName( + chainId, + chain, + )} executed a module transaction ${shortenAddress(txHash)} from module ${shortenAddress(module)}.`, + } + }, + [WebhookType.CONFIRMATION_REQUEST]: ({ address, safeTxHash, chainId }, chain) => { + return { + title: 'Confirmation request', + body: `Safe ${shortenAddress(address)} on ${getChainName( + chainId, + chain, + )} has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.`, + } + }, + [WebhookType.SAFE_CREATED]: () => { + // We do not preemptively subscribe to Safes before they are created + return null + }, +} diff --git a/src/service-workers/firebase-messaging/notifications.ts b/src/service-workers/firebase-messaging/notifications.ts index aca25d694a..c447632b1b 100644 --- a/src/service-workers/firebase-messaging/notifications.ts +++ b/src/service-workers/firebase-messaging/notifications.ts @@ -1,22 +1,28 @@ // Be careful what you import here as it will increase the service worker bundle size import { get as getFromIndexedDb } from 'idb-keyval' -import { formatUnits } from '@ethersproject/units' // Increases bundle significantly but unavoidable -import { getChainsConfig, getBalances, setBaseUrl } from '@safe-global/safe-gateway-typescript-sdk' -import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' -import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { getChainsConfig, setBaseUrl } from '@safe-global/safe-gateway-typescript-sdk' import type { MessagePayload } from 'firebase/messaging' import { AppRoutes } from '@/config/routes' // Has no internal imports -import { isWebhookEvent, WebhookType } from './webhook-types' +import { isWebhookEvent } from './webhook-types' import { getPushNotificationPrefsKey, createPushNotificationPrefsIndexedDb, } from '@/services/push-notifications/preferences' import { FIREBASE_IS_PRODUCTION } from '@/services/push-notifications/firebase' +import { Notifications } from './notification-mapper' import type { WebhookEvent } from './webhook-types' import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' +const GATEWAY_URL_PRODUCTION = process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' +const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' + +// localStorage cannot be accessed in service workers so we reference the flag from the environment +const GATEWAY_URL = FIREBASE_IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING + +setBaseUrl(GATEWAY_URL) + export const shouldShowServiceWorkerPushNotification = async (payload: MessagePayload): Promise => { if (!isWebhookEvent(payload.data)) { return true @@ -39,65 +45,6 @@ export const shouldShowServiceWorkerPushNotification = async (payload: MessagePa return preferencesStore.preferences[type] } -const GATEWAY_URL_PRODUCTION = process.env.NEXT_PUBLIC_GATEWAY_URL_PRODUCTION || 'https://safe-client.safe.global' -const GATEWAY_URL_STAGING = process.env.NEXT_PUBLIC_GATEWAY_URL_STAGING || 'https://safe-client.staging.5afe.dev' - -// localStorage cannot be accessed in service workers so we reference the flag from the environment -const GATEWAY_URL = FIREBASE_IS_PRODUCTION ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING -setBaseUrl(GATEWAY_URL) - -const getChain = async (chainId: string): Promise => { - return getChainsConfig() - .then(({ results }) => results.find((chain) => chain.chainId === chainId)) - .catch(() => undefined) -} - -const getTokenInfo = async ( - chainId: string, - safeAddress: string, - tokenAddress: string, - tokenValue?: string, -): Promise<{ symbol: string; value: string; name: string }> => { - const DEFAULT_CURRENCY = 'USD' - - const DEFAULT_INFO = { - symbol: 'tokens', - value: 'some', - name: 'Token', - } - - let tokenInfo: TokenInfo | undefined - - try { - const balances = await getBalances(chainId, safeAddress, DEFAULT_CURRENCY) - tokenInfo = balances.items.find((token) => token.tokenInfo.address === tokenAddress)?.tokenInfo - } catch { - // Swallow error - } - - if (!tokenInfo) { - return DEFAULT_INFO - } - - const symbol = tokenInfo?.symbol ?? DEFAULT_INFO.symbol - const value = tokenValue && tokenInfo ? formatUnits(tokenValue, tokenInfo.decimals).toString() : DEFAULT_INFO.value - const name = tokenInfo?.name ?? DEFAULT_INFO.name - - return { - symbol, - value, - name, - } -} - -const shortenAddress = (address: string, length = 4): string => { - if (!address) { - return '' - } - - return `${address.slice(0, length + 2)}...${address.slice(-length)}` -} - const getLink = (data: WebhookEvent, shortName?: string) => { const URL = self.location.origin @@ -116,108 +63,15 @@ const getLink = (data: WebhookEvent, shortName?: string) => { return withRoute(AppRoutes.transactions.history) } -type PushNotificationsMap = { - [P in T['type']]: ( - data: Extract, - ) => Promise<{ title: string; body: string }> | { title: string; body: string } | null -} - export const _parseServiceWorkerWebhookPushNotification = async ( data: WebhookEvent, ): Promise<{ title: string; body: string; link: string } | undefined> => { - const chain = await getChain(data.chainId) - - const chainName = chain?.chainName ?? `chain ${data.chainId}` - - const currencySymbol = chain?.nativeCurrency?.symbol ?? 'ETH' - const currencyName = chain?.nativeCurrency?.name ?? 'Ether' - - const Notifications: PushNotificationsMap = { - [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash }) => { - return { - title: 'Transaction confirmation', - body: `Safe ${shortenAddress(address)} on ${chainName} has a new confirmation from ${shortenAddress( - owner, - )} on transaction ${shortenAddress(safeTxHash)}.`, - } - }, - [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash }) => { - const didFail = failed === 'true' - return { - title: `Transaction ${didFail ? 'failed' : 'executed'}`, - body: `Safe ${shortenAddress(address)} on ${chainName} ${ - didFail ? 'failed to execute' : 'executed' - } transaction ${shortenAddress(txHash)}.`, - } - }, - [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash }) => { - return { - title: 'Pending transaction', - body: `Safe ${shortenAddress(address)} on ${chainName} has a pending transaction ${shortenAddress( - safeTxHash, - )}.`, - } - }, - [WebhookType.INCOMING_ETHER]: ({ address, txHash, value }) => { - return { - title: `${currencyName} received`, - body: `Safe ${shortenAddress(address)} on ${chainName} received ${formatUnits( - value, - chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, - } - }, - [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value }) => { - return { - title: `${currencyName} sent`, - body: `Safe ${shortenAddress(address)} on ${chainName} sent ${formatUnits( - value, - chain?.nativeCurrency?.decimals, - ).toString()} ${currencySymbol} in transaction ${shortenAddress(txHash)}.`, - } - }, - [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { - const token = await getTokenInfo(data.chainId, address, tokenAddress, value) - return { - title: `${token.name} received`, - body: `Safe ${shortenAddress(address)} on ${chainName} received ${token.value} ${ - token.symbol - } in transaction ${shortenAddress(txHash)}.`, - } - }, - [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value }) => { - const token = await getTokenInfo(data.chainId, address, tokenAddress, value) - return { - title: `${token.name} sent`, - body: `Safe ${shortenAddress(address)} on ${chainName} sent ${token.value} ${ - token.symbol - } in transaction ${shortenAddress(txHash)}.`, - } - }, - [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash }) => { - return { - title: 'Module transaction', - body: `Safe ${shortenAddress(address)} on ${chainName} executed a module transaction ${shortenAddress( - txHash, - )} from module ${shortenAddress(module)}.`, - } - }, - [WebhookType.CONFIRMATION_REQUEST]: ({ address, safeTxHash }) => { - return { - title: 'Confirmation request', - body: `Safe ${shortenAddress( - address, - )} on ${chainName} has a new confirmation request for transaction ${shortenAddress(safeTxHash)}.`, - } - }, - [WebhookType.SAFE_CREATED]: () => { - // We do not preemptively subscribe to Safes before they are created - return null - }, - } + const chain = await getChainsConfig() + .then(({ results }) => results.find((chain) => chain.chainId === data.chainId)) + .catch(() => undefined) // Can be safely casted as `data.type` is a mapped type of `NotificationsMap` - const notification = await Notifications[data.type](data as any) + const notification = await Notifications[data.type](data as any, chain) if (notification) { return { From 12116b71f0067a697e64c861fc682219a9014108 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 14 Sep 2023 12:28:06 +0200 Subject: [PATCH 46/62] fix: address comments --- package.json | 2 +- .../GlobalPushNotifications.tsx | 4 +- .../PushNotificationsBanner/index.tsx | 8 ++-- .../PushNotificationsBanner/styles.module.css | 1 - .../useNotificationPreferences.test.ts | 2 +- .../useNotificationRegistrations.test.ts | 18 ++++---- .../hooks/useNotificationPreferences.ts | 6 +-- .../hooks/useNotificationRegistrations.ts | 21 +++++---- .../settings/PushNotifications/index.tsx | 43 ++++++++++--------- .../settings/PushNotifications/logic.ts | 12 +----- src/hooks/useDecodeTx.ts | 2 +- src/hooks/useIsMac.ts | 13 ++++++ .../analytics/events/push-notifications.ts | 10 ----- 13 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 src/hooks/useIsMac.ts diff --git a/package.json b/package.json index 072fa218fa..2b62ff0bf7 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@safe-global/safe-core-sdk-utils": "^1.7.4", "@safe-global/safe-deployments": "1.25.0", "@safe-global/safe-ethers-lib": "^1.9.4", - "@safe-global/safe-gateway-typescript-sdk": "^3.11.0", + "@safe-global/safe-gateway-typescript-sdk": "^3.12.0", "@safe-global/safe-modules-deployments": "^1.0.0", "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.28.1", diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 695c84b939..3d7bac2eb3 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -261,13 +261,15 @@ export const GlobalPushNotifications = (): ReactElement | null => { return } + // Although the (un-)registration functions will request permission, + // we manually change beforehand prevent multiple promises from throwing const isGranted = await requestNotificationPermission() if (!isGranted) { return } - const registrationPromises: Array> = [] + const registrationPromises: Array> = [] const safesToRegister = getSafesToRegister(selectedSafes, currentNotifiedSafes) if (safesToRegister) { diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index 4caabc7420..f33d1f6d3f 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -41,11 +41,11 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } const dismissBanner = useCallback(() => { trackEvent(PUSH_NOTIFICATION_EVENTS.DISMISS_BANNER) - setDismissedBannerPerChain({ - ...dismissedBannerPerChain, + setDismissedBannerPerChain((prev) => ({ + ...prev, [safe.chainId]: true, - }) - }, [dismissedBannerPerChain, safe.chainId, setDismissedBannerPerChain]) + })) + }, [safe.chainId, setDismissedBannerPerChain]) // Click outside to dismiss banner useEffect(() => { diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css index 4e3a5a99da..821b9b6cd8 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css @@ -33,7 +33,6 @@ font-size: 12px; width: var(--space-5); height: 24px; - z-index: 9999999; position: relative; } diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index 160e329784..0ba27fc246 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -216,7 +216,7 @@ describe('useNotificationPreferences', () => { }) }) - it('should clearPreferences preferences, then hydrate the preferences state', async () => { + it('should delete all preferences, then hydrate the preferences state', async () => { const chainId1 = '1' const safeAddress1 = hexZeroPad('0x1', 20) const safeAddress2 = hexZeroPad('0x1', 20) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index 130b2d2bab..836f1ee479 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -302,13 +302,13 @@ describe('useNotificationRegistrations', () => { unregisterDeviceSpy.mockImplementation(() => Promise.resolve('Unregistration could not be completed.')) const uuid = self.crypto.randomUUID() - const clearPreferencesMock = jest.fn() + const deleteAllPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _clearPreferences: clearPreferencesMock, + _deleteAllPreferences: deleteAllPreferencesMock, } as unknown as ReturnType), ) @@ -318,20 +318,20 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(clearPreferencesMock).not.toHaveBeenCalled() + expect(deleteAllPreferencesMock).not.toHaveBeenCalled() }) it('does not clear preferences if unregistration throws', async () => { unregisterDeviceSpy.mockImplementation(() => Promise.reject()) const uuid = self.crypto.randomUUID() - const clearPreferencesMock = jest.fn() + const deleteAllPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _clearPreferences: clearPreferencesMock, + _deleteAllPreferences: deleteAllPreferencesMock, } as unknown as ReturnType), ) @@ -341,20 +341,20 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(clearPreferencesMock).not.toHaveBeenCalledWith() + expect(deleteAllPreferencesMock).not.toHaveBeenCalledWith() }) it('clears preferences if unregistration succeeds', async () => { unregisterDeviceSpy.mockImplementation(() => Promise.resolve()) const uuid = self.crypto.randomUUID() - const clearPreferencesMock = jest.fn() + const deleteAllPreferencesMock = jest.fn() ;(preferences.useNotificationPreferences as jest.Mock).mockImplementation( () => ({ uuid, - _clearPreferences: clearPreferencesMock, + _deleteAllPreferences: deleteAllPreferencesMock, } as unknown as ReturnType), ) @@ -364,7 +364,7 @@ describe('useNotificationRegistrations', () => { expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) - expect(clearPreferencesMock).toHaveBeenCalled() + expect(deleteAllPreferencesMock).toHaveBeenCalled() }) }) }) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index f55625236e..8f162e6057 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -50,7 +50,7 @@ export const useNotificationPreferences = (): { ) => void _createPreferences: (safesToRegister: NotifiableSafes) => void _deletePreferences: (safesToUnregister: NotifiableSafes) => void - _clearPreferences: () => void + _deleteAllPreferences: () => void } => { // State const uuid = useUuid() @@ -195,7 +195,7 @@ export const useNotificationPreferences = (): { } // Delete all preferences store entries - const clearPreferences = () => { + const deleteAllPreferences = () => { if (!preferencesStore) { return } @@ -212,6 +212,6 @@ export const useNotificationPreferences = (): { updatePreferences, _createPreferences: createPreferences, _deletePreferences: deletePreferences, - _clearPreferences: clearPreferences, + _deleteAllPreferences: deleteAllPreferences, } } diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index ac9a956979..7fb1b22b9c 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -11,7 +11,7 @@ import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' import type { NotifiableSafes } from '../logic' -const registrationFlow = async (registrationFn: Promise, callback: () => void) => { +const registrationFlow = async (registrationFn: Promise, callback: () => void): Promise => { let success = false try { @@ -27,16 +27,19 @@ const registrationFlow = async (registrationFn: Promise, callback: () => v if (success) { callback() } + + return success } + export const useNotificationRegistrations = (): { - registerNotifications: (safesToRegister: NotifiableSafes, withSignature?: boolean) => Promise - unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise - unregisterChainNotifications: (chainId: string) => Promise + registerNotifications: (safesToRegister: NotifiableSafes, withSignature?: boolean) => Promise + unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise + unregisterChainNotifications: (chainId: string) => Promise } => { const dispatch = useAppDispatch() const web3 = useWeb3() - const { uuid, _createPreferences, _deletePreferences, _clearPreferences } = useNotificationPreferences() + const { uuid, _createPreferences, _deletePreferences, _deleteAllPreferences } = useNotificationPreferences() const registerNotifications = async (safesToRegister: NotifiableSafes) => { if (!uuid || !web3) { @@ -53,7 +56,7 @@ export const useNotificationRegistrations = (): { return registerDevice(payload) } - await registrationFlow(register(), () => { + return registrationFlow(register(), () => { _createPreferences(safesToRegister) const totalRegistered = Object.values(safesToRegister).reduce( @@ -80,7 +83,7 @@ export const useNotificationRegistrations = (): { const unregisterSafeNotifications = async (chainId: string, safeAddress: string) => { if (uuid) { - await registrationFlow(unregisterSafe(chainId, safeAddress, uuid), () => { + return registrationFlow(unregisterSafe(chainId, safeAddress, uuid), () => { _deletePreferences({ [chainId]: [safeAddress] }) trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_SAFE) }) @@ -89,8 +92,8 @@ export const useNotificationRegistrations = (): { const unregisterChainNotifications = async (chainId: string) => { if (uuid) { - await registrationFlow(unregisterDevice(chainId, uuid), () => { - _clearPreferences() + return registrationFlow(unregisterDevice(chainId, uuid), () => { + _deleteAllPreferences() trackEvent(PUSH_NOTIFICATION_EVENTS.UNREGISTER_DEVICE) }) } diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index df3a9605aa..ad3ba6331f 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -22,11 +22,11 @@ import { GlobalPushNotifications } from './GlobalPushNotifications' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { IS_DEV } from '@/config/constants' import { useAppDispatch } from '@/store' -import { showNotification } from '@/store/notificationsSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { AppRoutes } from '@/config/routes' import CheckWallet from '@/components/common/CheckWallet' +import { useIsMac } from '@/hooks/useIsMac' import css from './styles.module.css' @@ -34,6 +34,7 @@ export const PushNotifications = (): ReactElement => { const dispatch = useAppDispatch() const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() + const isMac = useIsMac() const { updatePreferences, getPreferences, getAllPreferences } = useNotificationPreferences() const { unregisterSafeNotifications, unregisterChainNotifications, registerNotifications } = @@ -45,7 +46,6 @@ export const PushNotifications = (): ReactElement => { updatePreferences(safe.chainId, safe.address.value, newPreferences) } - const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') const shouldShowMacHelper = isMac || IS_DEV const handleOnChange = async () => { @@ -216,28 +216,29 @@ export const PushNotifications = (): ReactElement => { control={ { - registerNotifications({ - [safe.chainId]: [safe.address.value], - }) - .then(() => { - setPreferences({ - ...preferences, - [WebhookType.CONFIRMATION_REQUEST]: checked, - }) + onChange={(_, checked) => { + const updateConfirmationRequestPreferences = () => { + setPreferences({ + ...preferences, + [WebhookType.CONFIRMATION_REQUEST]: checked, + }) - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_CONFIRMATION_REQUEST, label: checked }) + trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_CONFIRMATION_REQUEST, label: checked }) + } - dispatch( - showNotification({ - message: - 'You will now receive notifications about confirmation requests for this Safe Account in your browser.', - variant: 'success', - groupKey: 'notifications', - }), - ) + if (checked) { + registerNotifications({ + [safe.chainId]: [safe.address.value], }) - .catch(() => null) + .then((registered) => { + if (registered) { + updateConfirmationRequestPreferences() + } + }) + .catch(() => null) + } else { + updateConfirmationRequestPreferences() + } }} /> } diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 9f2cf9cba6..61131e66f6 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -5,8 +5,6 @@ import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typ import type { Web3Provider } from '@ethersproject/providers' import { FIREBASE_VAPID_KEY, initializeFirebaseApp } from '@/services/push-notifications/firebase' -import { trackEvent } from '@/services/analytics' -import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import packageJson from '../../../../package.json' import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' @@ -29,15 +27,7 @@ export const requestNotificationPermission = async (): Promise => { logError(ErrorCodes._400, e) } - const isGranted = permission === 'granted' - - trackEvent(isGranted ? PUSH_NOTIFICATION_EVENTS.GRANT_PERMISSION : PUSH_NOTIFICATION_EVENTS.REJECT_PERMISSION) - - if (!isGranted) { - alert('You must allow notifications to register your device.') - } - - return isGranted + return permission === 'granted' } const getSafeRegistrationSignature = ({ diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts index fd8414d3ad..50b546eae7 100644 --- a/src/hooks/useDecodeTx.ts +++ b/src/hooks/useDecodeTx.ts @@ -14,7 +14,7 @@ const useDecodeTx = (tx?: SafeTransaction): AsyncResult => const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined const [data = nativeTransfer, error, loading] = useAsync(() => { - if (!encodedData || isEmptyData || !tx.data.to) return + if (!encodedData || isEmptyData) return return getDecodedData(chainId, encodedData, tx.data.to) }, [chainId, encodedData, isEmptyData, tx?.data.to]) diff --git a/src/hooks/useIsMac.ts b/src/hooks/useIsMac.ts new file mode 100644 index 0000000000..e324257c69 --- /dev/null +++ b/src/hooks/useIsMac.ts @@ -0,0 +1,13 @@ +import { useState, useEffect } from 'react' + +export const useIsMac = (): boolean => { + const [isMac, setIsMac] = useState(false) + + useEffect(() => { + if (typeof navigator !== 'undefined') { + setIsMac(navigator.userAgent.includes('Mac')) + } + }, []) + + return isMac +} diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts index d4884d1523..647c3e256f 100644 --- a/src/services/analytics/events/push-notifications.ts +++ b/src/services/analytics/events/push-notifications.ts @@ -11,16 +11,6 @@ export const PUSH_NOTIFICATION_EVENTS = { action: 'Click notification', category, }, - // User granted notification permissions - GRANT_PERMISSION: { - action: 'Allow notifications', - category, - }, - // User refused notification permissions - REJECT_PERMISSION: { - action: 'Reject notifications', - category, - }, // User registered Safe(s) for notifications REGISTER_SAFES: { action: 'Register Safe(s) notifications', From 93b614e6afaf00cf4332f1f5be73924a6717ec3a Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 14 Sep 2023 12:34:28 +0200 Subject: [PATCH 47/62] fix: regenerate lockfile --- src/styles/vars.css | 136 -------------------------------------------- yarn.lock | 8 +-- 2 files changed, 4 insertions(+), 140 deletions(-) diff --git a/src/styles/vars.css b/src/styles/vars.css index a37695c4e6..e69de29bb2 100644 --- a/src/styles/vars.css +++ b/src/styles/vars.css @@ -1,136 +0,0 @@ -/* This file is generated from the MUI theme colors. Do not edit directly. */ - -:root { - --color-text-primary: #121312; - --color-text-secondary: #a1a3a7; - --color-text-disabled: #dddee0; - --color-primary-dark: #3c3c3c; - --color-primary-main: #121312; - --color-primary-light: #636669; - --color-secondary-dark: #0fda6d; - --color-secondary-main: #12ff80; - --color-secondary-light: #b0ffc9; - --color-secondary-background: #effff4; - --color-border-main: #a1a3a7; - --color-border-light: #dcdee0; - --color-border-background: #f4f4f4; - --color-error-dark: #ac2c3b; - --color-error-main: #ff5f72; - --color-error-light: #ffb4bd; - --color-error-background: #ffe6ea; - --color-success-dark: #028d4c; - --color-success-main: #00b460; - --color-success-light: #72f5b8; - --color-success-background: #f2fff9; - --color-info-dark: #52bfdc; - --color-info-main: #5fddff; - --color-info-light: #b7f0ff; - --color-info-background: #effcff; - --color-warning-dark: #cd674e; - --color-warning-main: #ff8061; - --color-warning-light: #ffb7a6; - --color-warning-background: #fff0ed; - --color-background-default: #f4f4f4; - --color-background-main: #f4f4f4; - --color-background-paper: #ffffff; - --color-background-light: #effff4; - --color-backdrop-main: #636669; - --color-logo-main: #121312; - --color-logo-background: #eeeff0; - --color-static-main: #121312; - --space-1: 8px; - --space-2: 16px; - --space-3: 24px; - --space-4: 32px; - --space-5: 40px; - --space-6: 48px; - --space-7: 56px; - --space-8: 64px; - --space-9: 72px; - --space-10: 80px; - --space-11: 88px; - --space-12: 96px; -} - -[data-theme='dark'] { - --color-text-primary: #ffffff; - --color-text-secondary: #636669; - --color-text-disabled: #636669; - --color-primary-dark: #0cb259; - --color-primary-main: #12ff80; - --color-primary-light: #a1a3a7; - --color-secondary-dark: #636669; - --color-secondary-main: #ffffff; - --color-secondary-light: #12ff80; - --color-secondary-background: #303033; - --color-border-main: #636669; - --color-border-light: #303033; - --color-border-background: #121312; - --color-error-dark: #ac2c3b; - --color-error-main: #ff5f72; - --color-error-light: #ffb4bd; - --color-error-background: #2f2527; - --color-success-dark: #028d4c; - --color-success-main: #00b460; - --color-success-light: #81c784; - --color-success-background: #1f2920; - --color-info-dark: #52bfdc; - --color-info-main: #5fddff; - --color-info-light: #b7f0ff; - --color-info-background: #19252c; - --color-warning-dark: #cd674e; - --color-warning-main: #ff8061; - --color-warning-light: #ffb7a6; - --color-warning-background: #2f2318; - --color-background-default: #121312; - --color-background-main: #121312; - --color-background-paper: #1c1c1c; - --color-background-light: #1b2a22; - --color-backdrop-main: #636669; - --color-logo-main: #ffffff; - --color-logo-background: #303033; - --color-static-main: #121312; -} - -/* The same as above for the brief moment before JS loads */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme='light']) { - --color-text-primary: #ffffff; - --color-text-secondary: #636669; - --color-text-disabled: #636669; - --color-primary-dark: #0cb259; - --color-primary-main: #12ff80; - --color-primary-light: #a1a3a7; - --color-secondary-dark: #636669; - --color-secondary-main: #ffffff; - --color-secondary-light: #12ff80; - --color-secondary-background: #303033; - --color-border-main: #636669; - --color-border-light: #303033; - --color-border-background: #121312; - --color-error-dark: #ac2c3b; - --color-error-main: #ff5f72; - --color-error-light: #ffb4bd; - --color-error-background: #2f2527; - --color-success-dark: #028d4c; - --color-success-main: #00b460; - --color-success-light: #81c784; - --color-success-background: #1f2920; - --color-info-dark: #52bfdc; - --color-info-main: #5fddff; - --color-info-light: #b7f0ff; - --color-info-background: #19252c; - --color-warning-dark: #cd674e; - --color-warning-main: #ff8061; - --color-warning-light: #ffb7a6; - --color-warning-background: #2f2318; - --color-background-default: #121312; - --color-background-main: #121312; - --color-background-paper: #1c1c1c; - --color-background-light: #1b2a22; - --color-backdrop-main: #636669; - --color-logo-main: #ffffff; - --color-logo-background: #303033; - --color-static-main: #121312; - } -} diff --git a/yarn.lock b/yarn.lock index 699cefdfe7..9c1065535f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4530,10 +4530,10 @@ "@safe-global/safe-core-sdk-utils" "^1.7.4" ethers "5.7.2" -"@safe-global/safe-gateway-typescript-sdk@^3.11.0": - version "3.11.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.11.0.tgz#e713dd98098e2fa126d3776c48625ab149863aef" - integrity sha512-jdNilX8US4KigO3GNCasZD121jLemKTmnT2GEH4LRNHecKAcB3bLcpFF/sYpozB0J+WiXKye75/g7xakNDf/hQ== +"@safe-global/safe-gateway-typescript-sdk@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.12.0.tgz#aa767a32f4d10f4ec9a47ad7e32d547d3b51e94c" + integrity sha512-hExCo62lScVC9/ztVqYEYL2pFxcqLTvB8fj0WtdP5FWrvbtEgD0pbVolchzD5bf85pbzvEwdAxSVS7EdCZxTNw== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.7.0" From cc5fa6612f1e38d42e0cf30aa66da84d1d43f176 Mon Sep 17 00:00:00 2001 From: iamacook Date: Thu, 14 Sep 2023 17:31:48 +0200 Subject: [PATCH 48/62] fix: address comments --- .../GlobalPushNotifications.tsx | 8 +- .../useNotificationPreferences.test.ts | 20 +++ .../useNotificationRegistrations.test.ts | 10 +- .../hooks/useNotificationPreferences.ts | 30 ++-- .../hooks/useNotificationRegistrations.ts | 8 +- .../settings/PushNotifications/index.tsx | 24 +++- .../settings/PushNotifications/logic.ts | 9 +- src/services/exceptions/ErrorCodes.ts | 2 + src/styles/vars.css | 136 ++++++++++++++++++ 9 files changed, 218 insertions(+), 29 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 3d7bac2eb3..6421615f1d 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -192,7 +192,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { const addedSafes = useAppSelector(selectAllAddedSafes) const { getAllPreferences } = useNotificationPreferences() - const { unregisterChainNotifications, unregisterSafeNotifications, registerNotifications } = + const { unregisterDeviceNotifications, unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() // Safes selected in the UI @@ -261,8 +261,8 @@ export const GlobalPushNotifications = (): ReactElement | null => { return } - // Although the (un-)registration functions will request permission, - // we manually change beforehand prevent multiple promises from throwing + // Although the (un-)registration functions will request permission in getToken we manually + // check beforehand to prevent multiple promises in registrationPromises from throwing const isGranted = await requestNotificationPermission() if (!isGranted) { @@ -280,7 +280,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { if (safesToUnregister) { const unregistrationPromises = Object.entries(safesToUnregister).flatMap(([chainId, safeAddresses]) => { if (shouldUnregisterDevice(chainId, safeAddresses, currentNotifiedSafes)) { - return unregisterChainNotifications(chainId) + return unregisterDeviceNotifications(chainId) } return safeAddresses.map((safeAddress) => unregisterSafeNotifications(chainId, safeAddress)) }) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index 0ba27fc246..bce8e91ea6 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -137,6 +137,26 @@ describe('useNotificationPreferences', () => { }) }) + it('should not create preferences when passed an empty object', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + result.current._createPreferences({}) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({}) + }) + }) + + it('should not create preferences when passed an empty array of Safes', async () => { + const { result } = renderHook(() => useNotificationPreferences()) + + result.current._createPreferences({ ['1']: [] }) + + await waitFor(() => { + expect(result.current.getAllPreferences()).toEqual({}) + }) + }) + it('should update preferences, then hydrate the preferences state', async () => { const chainId = '1' const safeAddress = hexZeroPad('0x1', 20) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index 836f1ee479..bfff0b0455 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -279,7 +279,7 @@ describe('useNotificationRegistrations', () => { }) }) - describe('unregisterChainNotifications', () => { + describe('unregisterDeviceNotifications', () => { const unregisterDeviceSpy = jest.spyOn(sdk, 'unregisterDevice') it('does not unregister device if no uuid is present', async () => { @@ -292,7 +292,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterChainNotifications('1') + await result.current.unregisterDeviceNotifications('1') expect(unregisterDeviceSpy).not.toHaveBeenCalled() }) @@ -314,7 +314,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterChainNotifications('1') + await result.current.unregisterDeviceNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) @@ -337,7 +337,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterChainNotifications('1') + await result.current.unregisterDeviceNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) @@ -360,7 +360,7 @@ describe('useNotificationRegistrations', () => { const { result } = renderHook(() => useNotificationRegistrations()) - await result.current.unregisterChainNotifications('1') + await result.current.unregisterDeviceNotifications('1') expect(unregisterDeviceSpy).toHaveBeenCalledWith('1', uuid) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index 8f162e6057..79ba979fad 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -15,6 +15,8 @@ import { createPushNotificationUuidIndexedDb, getPushNotificationPrefsKey, } from '@/services/push-notifications/preferences' +import { logError } from '@/services/exceptions' +import ErrorCodes from '@/services/exceptions/ErrorCodes' import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' @@ -101,7 +103,9 @@ export const useNotificationPreferences = (): { .then(() => { setUuid(_uuid) }) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._705, e) + }) }, [uuidStore]) // Hydrate UUID state @@ -121,7 +125,9 @@ export const useNotificationPreferences = (): { .then((preferencesEntries) => { setPreferences(Object.fromEntries(preferencesEntries)) }) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._705, e) + }) }, [preferencesStore]) // Hydrate preferences state @@ -130,7 +136,7 @@ export const useNotificationPreferences = (): { }, [hydratePreferences]) // Add store entry with default preferences for specified Safe(s) - const createPreferences = (safesToRegister: { [chain: string]: Array }) => { + const createPreferences = (safesToRegister: NotifiableSafes) => { if (!preferencesStore) { return } @@ -153,7 +159,9 @@ export const useNotificationPreferences = (): { setManyIndexedDb(defaultPreferencesEntries, preferencesStore) .then(hydratePreferences) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._706, e) + }) } // Update preferences for specified Safe @@ -176,11 +184,13 @@ export const useNotificationPreferences = (): { setIndexedDb(key, newPreferences, preferencesStore) .then(hydratePreferences) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._706, e) + }) } // Delete preferences store entry for specified Safe(s) - const deletePreferences = (safesToUnregister: { [chain: string]: Array }) => { + const deletePreferences = (safesToUnregister: NotifiableSafes) => { if (!preferencesStore) { return } @@ -191,7 +201,9 @@ export const useNotificationPreferences = (): { deleteManyFromIndexedDb(keysToDelete, preferencesStore) .then(hydratePreferences) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._706, e) + }) } // Delete all preferences store entries @@ -202,7 +214,9 @@ export const useNotificationPreferences = (): { clearIndexedDb(preferencesStore) .then(hydratePreferences) - .catch(() => null) + .catch((e) => { + logError(ErrorCodes._706, e) + }) } return { diff --git a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts index 7fb1b22b9c..30d0fa520d 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationRegistrations.ts @@ -11,7 +11,7 @@ import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' import type { NotifiableSafes } from '../logic' -const registrationFlow = async (registrationFn: Promise, callback: () => void): Promise => { +const registrationFlow = async (registrationFn: Promise, callback: () => void): Promise => { let success = false try { @@ -34,7 +34,7 @@ const registrationFlow = async (registrationFn: Promise, callback: () => v export const useNotificationRegistrations = (): { registerNotifications: (safesToRegister: NotifiableSafes, withSignature?: boolean) => Promise unregisterSafeNotifications: (chainId: string, safeAddress: string) => Promise - unregisterChainNotifications: (chainId: string) => Promise + unregisterDeviceNotifications: (chainId: string) => Promise } => { const dispatch = useAppDispatch() const web3 = useWeb3() @@ -90,7 +90,7 @@ export const useNotificationRegistrations = (): { } } - const unregisterChainNotifications = async (chainId: string) => { + const unregisterDeviceNotifications = async (chainId: string) => { if (uuid) { return registrationFlow(unregisterDevice(chainId, uuid), () => { _deleteAllPreferences() @@ -102,6 +102,6 @@ export const useNotificationRegistrations = (): { return { registerNotifications, unregisterSafeNotifications, - unregisterChainNotifications, + unregisterDeviceNotifications, } } diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index ad3ba6331f..3a62d31590 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -11,6 +11,7 @@ import { Link as MuiLink, } from '@mui/material' import Link from 'next/link' +import { useState } from 'react' import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' @@ -21,7 +22,6 @@ import { useNotificationPreferences } from './hooks/useNotificationPreferences' import { GlobalPushNotifications } from './GlobalPushNotifications' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { IS_DEV } from '@/config/constants' -import { useAppDispatch } from '@/store' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { AppRoutes } from '@/config/routes' @@ -31,27 +31,36 @@ import { useIsMac } from '@/hooks/useIsMac' import css from './styles.module.css' export const PushNotifications = (): ReactElement => { - const dispatch = useAppDispatch() const { safe, safeLoaded } = useSafeInfo() const isOwner = useIsSafeOwner() const isMac = useIsMac() + const [isRegistering, setIsRegistering] = useState(false) + const [isUpdatingIndexedDb, setIsUpdatingIndexedDb] = useState(false) const { updatePreferences, getPreferences, getAllPreferences } = useNotificationPreferences() - const { unregisterSafeNotifications, unregisterChainNotifications, registerNotifications } = + const { unregisterSafeNotifications, unregisterDeviceNotifications, registerNotifications } = useNotificationRegistrations() const preferences = getPreferences(safe.chainId, safe.address.value) const setPreferences = (newPreferences: NonNullable>) => { + setIsUpdatingIndexedDb(true) + updatePreferences(safe.chainId, safe.address.value, newPreferences) + + setIsUpdatingIndexedDb(false) } const shouldShowMacHelper = isMac || IS_DEV + const isCheckboxDisabled = isRegistering || isUpdatingIndexedDb const handleOnChange = async () => { + setIsRegistering(true) + if (!preferences) { await registerNotifications({ [safe.chainId]: [safe.address.value] }) trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_SAFE) + setIsRegistering(false) return } @@ -62,12 +71,13 @@ export const PushNotifications = (): ReactElement => { const shouldUnregisterDevice = totalRegisteredSafesOnChain === 1 if (shouldUnregisterDevice) { - await unregisterChainNotifications(safe.chainId) + await unregisterDeviceNotifications(safe.chainId) } else { await unregisterSafeNotifications(safe.chainId, safe.address.value) } trackEvent(PUSH_NOTIFICATION_EVENTS.DISABLE_SAFE) + setIsRegistering(false) } return ( @@ -116,7 +126,7 @@ export const PushNotifications = (): ReactElement => { } label={preferences ? 'On' : 'Off'} - disabled={!isOk} + disabled={!isOk || isRegistering} /> )} @@ -154,6 +164,7 @@ export const PushNotifications = (): ReactElement => { control={ { setPreferences({ ...preferences, @@ -178,6 +189,7 @@ export const PushNotifications = (): ReactElement => { preferences[WebhookType.EXECUTED_MULTISIG_TRANSACTION] && preferences[WebhookType.PENDING_MULTISIG_TRANSACTION] } + disabled={isUpdatingIndexedDb} onChange={(_, checked) => { setPreferences({ ...preferences, @@ -199,6 +211,7 @@ export const PushNotifications = (): ReactElement => { control={ { setPreferences({ ...preferences, @@ -216,6 +229,7 @@ export const PushNotifications = (): ReactElement => { control={ { const updateConfirmationRequestPreferences = () => { setPreferences({ diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index 61131e66f6..d6792b7fd7 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -45,6 +45,12 @@ const getSafeRegistrationSignature = ({ }) => { const MESSAGE_PREFIX = 'gnosis-safe' + // Signature must sign `keccack256('gnosis-safe{timestamp-epoch}{uuid}{cloud_messaging_token}{safes_sorted}': + // - `{timestamp-epoch}` must be an integer (no milliseconds) + // - `{safes_sorted}` must be checksummed safe addresses sorted and joined with no spaces + + // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 + const message = MESSAGE_PREFIX + timestamp + uuid + token + safeAddresses.join('') const hashedMessage = keccak256(toUtf8Bytes(message)) @@ -79,9 +85,6 @@ export const getRegisterDevicePayload = async ({ // If uuid is not provided a new device will be created. // If a uuid for an existing Safe is provided the FirebaseDevice will be updated with all the new data provided. // Safes provided on the request are always added and never removed/replaced - // Signature must sign `keccack256('gnosis-safe{timestamp-epoch}{uuid}{cloud_messaging_token}{safes_sorted}': - // - `{timestamp-epoch}` must be an integer (no milliseconds) - // - `{safes_sorted}` must be checksummed safe addresses sorted and joined with no spaces // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index a45f9ab9c9..628b3b2d1d 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -44,6 +44,8 @@ enum ErrorCodes { _702 = '702: Failed to remove from local/session storage', _703 = '703: Error importing an address book', _704 = '704: Error importing global data', + _705 = '704: Failed to read from IndexedDB', + _706 = '704: Failed to write to IndexedDB', _800 = '800: Safe creation tx failed', _801 = '801: Failed to send a tx with a spending limit', diff --git a/src/styles/vars.css b/src/styles/vars.css index e69de29bb2..a37695c4e6 100644 --- a/src/styles/vars.css +++ b/src/styles/vars.css @@ -0,0 +1,136 @@ +/* This file is generated from the MUI theme colors. Do not edit directly. */ + +:root { + --color-text-primary: #121312; + --color-text-secondary: #a1a3a7; + --color-text-disabled: #dddee0; + --color-primary-dark: #3c3c3c; + --color-primary-main: #121312; + --color-primary-light: #636669; + --color-secondary-dark: #0fda6d; + --color-secondary-main: #12ff80; + --color-secondary-light: #b0ffc9; + --color-secondary-background: #effff4; + --color-border-main: #a1a3a7; + --color-border-light: #dcdee0; + --color-border-background: #f4f4f4; + --color-error-dark: #ac2c3b; + --color-error-main: #ff5f72; + --color-error-light: #ffb4bd; + --color-error-background: #ffe6ea; + --color-success-dark: #028d4c; + --color-success-main: #00b460; + --color-success-light: #72f5b8; + --color-success-background: #f2fff9; + --color-info-dark: #52bfdc; + --color-info-main: #5fddff; + --color-info-light: #b7f0ff; + --color-info-background: #effcff; + --color-warning-dark: #cd674e; + --color-warning-main: #ff8061; + --color-warning-light: #ffb7a6; + --color-warning-background: #fff0ed; + --color-background-default: #f4f4f4; + --color-background-main: #f4f4f4; + --color-background-paper: #ffffff; + --color-background-light: #effff4; + --color-backdrop-main: #636669; + --color-logo-main: #121312; + --color-logo-background: #eeeff0; + --color-static-main: #121312; + --space-1: 8px; + --space-2: 16px; + --space-3: 24px; + --space-4: 32px; + --space-5: 40px; + --space-6: 48px; + --space-7: 56px; + --space-8: 64px; + --space-9: 72px; + --space-10: 80px; + --space-11: 88px; + --space-12: 96px; +} + +[data-theme='dark'] { + --color-text-primary: #ffffff; + --color-text-secondary: #636669; + --color-text-disabled: #636669; + --color-primary-dark: #0cb259; + --color-primary-main: #12ff80; + --color-primary-light: #a1a3a7; + --color-secondary-dark: #636669; + --color-secondary-main: #ffffff; + --color-secondary-light: #12ff80; + --color-secondary-background: #303033; + --color-border-main: #636669; + --color-border-light: #303033; + --color-border-background: #121312; + --color-error-dark: #ac2c3b; + --color-error-main: #ff5f72; + --color-error-light: #ffb4bd; + --color-error-background: #2f2527; + --color-success-dark: #028d4c; + --color-success-main: #00b460; + --color-success-light: #81c784; + --color-success-background: #1f2920; + --color-info-dark: #52bfdc; + --color-info-main: #5fddff; + --color-info-light: #b7f0ff; + --color-info-background: #19252c; + --color-warning-dark: #cd674e; + --color-warning-main: #ff8061; + --color-warning-light: #ffb7a6; + --color-warning-background: #2f2318; + --color-background-default: #121312; + --color-background-main: #121312; + --color-background-paper: #1c1c1c; + --color-background-light: #1b2a22; + --color-backdrop-main: #636669; + --color-logo-main: #ffffff; + --color-logo-background: #303033; + --color-static-main: #121312; +} + +/* The same as above for the brief moment before JS loads */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --color-text-primary: #ffffff; + --color-text-secondary: #636669; + --color-text-disabled: #636669; + --color-primary-dark: #0cb259; + --color-primary-main: #12ff80; + --color-primary-light: #a1a3a7; + --color-secondary-dark: #636669; + --color-secondary-main: #ffffff; + --color-secondary-light: #12ff80; + --color-secondary-background: #303033; + --color-border-main: #636669; + --color-border-light: #303033; + --color-border-background: #121312; + --color-error-dark: #ac2c3b; + --color-error-main: #ff5f72; + --color-error-light: #ffb4bd; + --color-error-background: #2f2527; + --color-success-dark: #028d4c; + --color-success-main: #00b460; + --color-success-light: #81c784; + --color-success-background: #1f2920; + --color-info-dark: #52bfdc; + --color-info-main: #5fddff; + --color-info-light: #b7f0ff; + --color-info-background: #19252c; + --color-warning-dark: #cd674e; + --color-warning-main: #ff8061; + --color-warning-light: #ffb7a6; + --color-warning-background: #2f2318; + --color-background-default: #121312; + --color-background-main: #121312; + --color-background-paper: #1c1c1c; + --color-background-light: #1b2a22; + --color-backdrop-main: #636669; + --color-logo-main: #ffffff; + --color-logo-background: #303033; + --color-static-main: #121312; + } +} From a8ed67ac5f515dd22b8049eeced7e8ec1e06dee0 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Mon, 18 Sep 2023 09:14:34 +0200 Subject: [PATCH 49/62] feat: add push notification tracking (#2500) * feat: add push notification tracking * fix: address review comments * fix: error message --- .../useNotificationPreferences.test.ts | 38 +++---- .../__tests__/useNotificationTracking.test.ts | 106 ++++++++++++++++++ .../hooks/useNotificationPreferences.ts | 6 +- .../hooks/useNotificationTracking.ts | 76 +++++++++++++ src/pages/_app.tsx | 2 + .../firebase-messaging-sw.ts | 23 +++- .../analytics/events/push-notifications.ts | 6 +- src/services/analytics/gtm.ts | 1 + src/services/analytics/types.ts | 1 + src/services/exceptions/ErrorCodes.ts | 1 + src/services/push-notifications/tracking.ts | 71 ++++++++++++ 11 files changed, 300 insertions(+), 31 deletions(-) create mode 100644 src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts create mode 100644 src/components/settings/PushNotifications/hooks/useNotificationTracking.ts create mode 100644 src/services/push-notifications/tracking.ts diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index bce8e91ea6..d17ca713df 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -9,7 +9,7 @@ import { } from '@/services/push-notifications/preferences' import { useNotificationPreferences, - _DEFAULT_NOTIFICATION_PREFERENCES, + DEFAULT_NOTIFICATION_PREFERENCES, _setPreferences, _setUuid, } from '../useNotificationPreferences' @@ -66,7 +66,7 @@ describe('useNotificationPreferences', () => { [`${chainId}:${safeAddress}`]: { chainId, safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } @@ -87,7 +87,7 @@ describe('useNotificationPreferences', () => { [`${chainId}:${safeAddress}`]: { chainId, safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } @@ -121,17 +121,17 @@ describe('useNotificationPreferences', () => { [`${chainId1}:${safeAddress1}`]: { chainId: chainId1, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId1}:${safeAddress2}`]: { chainId: chainId1, safeAddress: safeAddress2, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, }) }) @@ -165,7 +165,7 @@ describe('useNotificationPreferences', () => { [`${chainId}:${safeAddress}`]: { chainId: chainId, safeAddress: safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } @@ -174,7 +174,7 @@ describe('useNotificationPreferences', () => { const { result } = renderHook(() => useNotificationPreferences()) result.current.updatePreferences(chainId, safeAddress, { - ..._DEFAULT_NOTIFICATION_PREFERENCES, + ...DEFAULT_NOTIFICATION_PREFERENCES, [WebhookType.NEW_CONFIRMATION]: false, }) @@ -184,7 +184,7 @@ describe('useNotificationPreferences', () => { chainId: chainId, safeAddress: safeAddress, preferences: { - ..._DEFAULT_NOTIFICATION_PREFERENCES, + ...DEFAULT_NOTIFICATION_PREFERENCES, [WebhookType.NEW_CONFIRMATION]: false, }, }, @@ -203,17 +203,17 @@ describe('useNotificationPreferences', () => { [`${chainId1}:${safeAddress1}`]: { chainId: chainId1, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId1}:${safeAddress2}`]: { chainId: chainId1, safeAddress: safeAddress2, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } @@ -230,7 +230,7 @@ describe('useNotificationPreferences', () => { [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, }) }) @@ -247,17 +247,17 @@ describe('useNotificationPreferences', () => { [`${chainId1}:${safeAddress1}`]: { chainId: chainId1, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId1}:${safeAddress2}`]: { chainId: chainId1, safeAddress: safeAddress2, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } @@ -292,17 +292,17 @@ describe('useNotificationPreferences', () => { [`${chainId1}:${safeAddress1}`]: { chainId: chainId1, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId1}:${safeAddress2}`]: { chainId: chainId1, safeAddress: safeAddress2, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, [`${chainId2}:${safeAddress1}`]: { chainId: chainId2, safeAddress: safeAddress1, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, }, } diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts new file mode 100644 index 0000000000..c32a3897fd --- /dev/null +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts @@ -0,0 +1,106 @@ +import 'fake-indexeddb/auto' +import { entries, setMany } from 'idb-keyval' + +import * as tracking from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import { createNotificationTrackingIndexedDb } from '@/services/push-notifications/tracking' +import { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' +import { renderHook, waitFor } from '@/tests/test-utils' +import { useNotificationTracking } from '../useNotificationTracking' + +jest.mock('@/services/analytics', () => ({ + trackEvent: jest.fn(), +})) + +describe('useNotificationTracking', () => { + beforeEach(() => { + // Reset indexedDB + indexedDB = new IDBFactory() + jest.clearAllMocks() + }) + + it('should track all cached events and clear the cache', async () => { + jest.spyOn(tracking, 'trackEvent') + + const cache = { + [`1:${WebhookType.INCOMING_ETHER}`]: { + shown: 1, + opened: 0, + }, + [`2:${WebhookType.OUTGOING_ETHER}`]: { + shown: 0, + opened: 1, + }, + [`3:${WebhookType.INCOMING_TOKEN}`]: { + shown: 1, + opened: 1, + }, + [`137:${WebhookType.OUTGOING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + } + + await setMany(Object.entries(cache), createNotificationTrackingIndexedDb()) + + renderHook(() => useNotificationTracking()) + + await waitFor(() => { + expect(tracking.trackEvent).toHaveBeenCalledTimes(4) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: WebhookType.INCOMING_ETHER, + chainId: '1', + }) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: WebhookType.OUTGOING_ETHER, + chainId: '2', + }) + + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: WebhookType.INCOMING_TOKEN, + chainId: '3', + }) + expect(tracking.trackEvent).toHaveBeenCalledWith({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: WebhookType.INCOMING_TOKEN, + chainId: '3', + }) + }) + + const _entries = await entries(createNotificationTrackingIndexedDb()) + expect(Object.fromEntries(_entries)).toStrictEqual({ + [`1:${WebhookType.INCOMING_ETHER}`]: { + shown: 0, + opened: 0, + }, + [`2:${WebhookType.OUTGOING_ETHER}`]: { + shown: 0, + opened: 0, + }, + [`3:${WebhookType.INCOMING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + [`137:${WebhookType.OUTGOING_TOKEN}`]: { + shown: 0, + opened: 0, + }, + }) + }) + + it('should not track if no cache exists', async () => { + jest.spyOn(tracking, 'trackEvent') + + const _entries = await entries(createNotificationTrackingIndexedDb()) + expect(_entries).toStrictEqual([]) + + renderHook(() => useNotificationTracking()) + + expect(tracking.trackEvent).not.toHaveBeenCalled() + }) +}) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index 79ba979fad..8b2c7378f5 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -20,7 +20,7 @@ import ErrorCodes from '@/services/exceptions/ErrorCodes' import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' -export const _DEFAULT_NOTIFICATION_PREFERENCES: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'] = { +export const DEFAULT_NOTIFICATION_PREFERENCES: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'] = { [WebhookType.NEW_CONFIRMATION]: true, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, @@ -44,7 +44,7 @@ export const _setPreferences = setPreferences export const useNotificationPreferences = (): { uuid: string | undefined getAllPreferences: () => PushNotificationPreferences | undefined - getPreferences: (chainId: string, safeAddress: string) => typeof _DEFAULT_NOTIFICATION_PREFERENCES | undefined + getPreferences: (chainId: string, safeAddress: string) => typeof DEFAULT_NOTIFICATION_PREFERENCES | undefined updatePreferences: ( chainId: string, safeAddress: string, @@ -149,7 +149,7 @@ export const useNotificationPreferences = (): { const defaultPreferences: PushNotificationPreferences[PushNotificationPrefsKey] = { chainId, safeAddress, - preferences: _DEFAULT_NOTIFICATION_PREFERENCES, + preferences: DEFAULT_NOTIFICATION_PREFERENCES, } return [key, defaultPreferences] diff --git a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts new file mode 100644 index 0000000000..773d9bc4df --- /dev/null +++ b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts @@ -0,0 +1,76 @@ +import { keys as keysFromIndexedDb, update as updateIndexedDb } from 'idb-keyval' +import { useEffect } from 'react' + +import { + DEFAULT_WEBHOOK_TRACKING, + createNotificationTrackingIndexedDb, + parseNotificationTrackingKey, +} from '@/services/push-notifications/tracking' +import { trackEvent } from '@/services/analytics' +import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' +import ErrorCodes from '@/services/exceptions/ErrorCodes' +import { logError } from '@/services/exceptions' +import type { NotificationTracking, NotificationTrackingKey } from '@/services/push-notifications/tracking' +import type { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' + +const trackNotificationEvents = ( + chainId: string, + type: WebhookType, + notificationCount: NotificationTracking[NotificationTrackingKey], +) => { + // Shown notifications + for (let i = 0; i < notificationCount.shown; i++) { + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, + label: type, + chainId, + }) + } + + // Opened notifications + for (let i = 0; i < notificationCount.opened; i++) { + trackEvent({ + ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, + label: type, + chainId, + }) + } +} + +const handleTrackCachedNotificationEvents = async ( + trackingStore: ReturnType, +) => { + try { + // Get all tracked webhook events by chainId, e.g. "1:NEW_CONFIRMATION" + const trackedNotificationKeys = await keysFromIndexedDb(trackingStore) + + // Get the number of notifications shown/opened and track then clear the cache + const promises = trackedNotificationKeys.map((key) => { + return updateIndexedDb( + key, + (notificationCount) => { + if (notificationCount) { + const { chainId, type } = parseNotificationTrackingKey(key) + trackNotificationEvents(chainId, type, notificationCount) + } + + // Return the default cache with 0 shown/opened events + return DEFAULT_WEBHOOK_TRACKING + }, + trackingStore, + ) + }) + + await Promise.all(promises) + } catch (e) { + logError(ErrorCodes._401, e) + } +} + +export const useNotificationTracking = (): void => { + useEffect(() => { + if (typeof indexedDB !== 'undefined') { + handleTrackCachedNotificationEvents(createNotificationTrackingIndexedDb()) + } + }, []) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d6612c4179..602e6f0da9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,6 +37,7 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' +import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -44,6 +45,7 @@ const InitApp = (): null => { setGatewayBaseUrl(GATEWAY_URL) useAdjustUrl() useGtm() + useNotificationTracking() useInitSession() useLoadableStores() useInitOnboard() diff --git a/src/service-workers/firebase-messaging/firebase-messaging-sw.ts b/src/service-workers/firebase-messaging/firebase-messaging-sw.ts index 6c0d6e8fe5..e7c59aa109 100644 --- a/src/service-workers/firebase-messaging/firebase-messaging-sw.ts +++ b/src/service-workers/firebase-messaging/firebase-messaging-sw.ts @@ -3,15 +3,21 @@ /// import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw' +import type { MessagePayload } from 'firebase/messaging/sw' import { initializeFirebaseApp } from '@/services/push-notifications/firebase' import { shouldShowServiceWorkerPushNotification, parseServiceWorkerPushNotification, } from '@/service-workers/firebase-messaging/notifications' +import { cacheServiceWorkerPushNotificationTrackingEvent } from '@/services/push-notifications/tracking' declare const self: ServiceWorkerGlobalScope +type NotificationData = MessagePayload['data'] & { + link: string +} + export function firebaseMessagingSw() { const ICON_PATH = '/images/safe-logo-green.png' @@ -27,13 +33,11 @@ export function firebaseMessagingSw() { (event) => { event.notification.close() - const link = event.notification.tag + const data: NotificationData = event.notification.data - if (!link) { - return - } + cacheServiceWorkerPushNotificationTrackingEvent('opened', data) - self.clients.openWindow(link) + self.clients.openWindow(data.link) }, false, ) @@ -53,11 +57,18 @@ export function firebaseMessagingSw() { return } + const data: NotificationData = { + ...payload.data, + link: notification.link ?? self.location.origin, + } + + cacheServiceWorkerPushNotificationTrackingEvent('shown', data) + self.registration.showNotification(notification.title || '', { icon: ICON_PATH, body: notification.body, image: notification.image, - tag: notification.link ?? self.location.origin, + data, }) }) } diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts index 647c3e256f..f401f7dee6 100644 --- a/src/services/analytics/events/push-notifications.ts +++ b/src/services/analytics/events/push-notifications.ts @@ -6,9 +6,9 @@ export const PUSH_NOTIFICATION_EVENTS = { action: 'Show notification', category, }, - // User clicked on notification - CLICK_NOTIFICATION: { - action: 'Click notification', + // User opened on notification + OPEN_NOTIFICATION: { + action: 'Open notification', category, }, // User registered Safe(s) for notifications diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 2d376cff41..232a247766 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -104,6 +104,7 @@ export const gtmTrack = (eventData: AnalyticsEvent): void => { event: eventData.event || EventType.CLICK, eventCategory: eventData.category, eventAction: eventData.action, + chainId: eventData.chainId || commonEventParams.chainId, } if (eventData.event) { diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index 62e43d6d50..b2df3d6c6e 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -15,6 +15,7 @@ export type AnalyticsEvent = { category: string action: string label?: EventLabel + chainId?: string } export type SafeAppSDKEvent = { diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 628b3b2d1d..1850f53c92 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -18,6 +18,7 @@ enum ErrorCodes { _303 = '303: Error creating pairing session', _400 = '400: Error requesting browser notification permissions', + _401 = '401: Error tracking push notifications', _600 = '600: Error fetching Safe info', _601 = '601: Error fetching balances', diff --git a/src/services/push-notifications/tracking.ts b/src/services/push-notifications/tracking.ts new file mode 100644 index 0000000000..6d9755406e --- /dev/null +++ b/src/services/push-notifications/tracking.ts @@ -0,0 +1,71 @@ +// Be careful what you import here as it will increase the service worker bundle size + +import { createStore as createIndexedDb, update as updateIndexedDb } from 'idb-keyval' +import type { MessagePayload } from 'firebase/messaging/sw' + +import { isWebhookEvent, WebhookType } from '@/service-workers/firebase-messaging/webhook-types' + +export type NotificationTrackingKey = `${string}:${WebhookType}` + +export type NotificationTracking = { + [chainKey: NotificationTrackingKey]: { + shown: number + opened: number + } +} + +export const getNotificationTrackingKey = (chainId: string, type: WebhookType): NotificationTrackingKey => { + return `${chainId}:${type}` +} + +export const parseNotificationTrackingKey = (key: string): { chainId: string; type: WebhookType } => { + const [chainId, type] = key.split(':') + + if (!Object.keys(WebhookType).includes(type)) { + throw new Error(`Invalid notification tracking key: ${key}`) + } + + return { + chainId, + type: type as WebhookType, + } +} + +export const createNotificationTrackingIndexedDb = () => { + const DB_NAME = 'notifications-tracking-database' + const STORE_NAME = 'notifications-tracking-store' + + return createIndexedDb(DB_NAME, STORE_NAME) +} + +export const DEFAULT_WEBHOOK_TRACKING: NotificationTracking[NotificationTrackingKey] = { + shown: 0, + opened: 0, +} + +export const cacheServiceWorkerPushNotificationTrackingEvent = ( + property: keyof NotificationTracking[NotificationTrackingKey], + data: MessagePayload['data'], +) => { + if (!isWebhookEvent(data)) { + return + } + + const key = getNotificationTrackingKey(data.chainId, data.type) + const store = createNotificationTrackingIndexedDb() + + updateIndexedDb( + key, + (notificationCount) => { + if (notificationCount) { + return { + ...notificationCount, + [property]: (notificationCount[property] ?? 0) + 1, + } + } + + return DEFAULT_WEBHOOK_TRACKING + }, + store, + ).catch(() => null) +} From f7836a2569a382fa30eecc3c05a132701b7386a2 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 18 Sep 2023 10:34:03 +0200 Subject: [PATCH 50/62] feat: add close button to banner --- .../PushNotificationsBanner/index.tsx | 12 +++++------- .../PushNotificationsBanner/styles.module.css | 7 +++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index f33d1f6d3f..ea1fe79a3d 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -1,4 +1,4 @@ -import { Button, Chip, Grid, SvgIcon, Typography } from '@mui/material' +import { Button, Chip, Grid, SvgIcon, Typography, IconButton } from '@mui/material' import Link from 'next/link' import { useRouter } from 'next/router' import { useCallback, useEffect } from 'react' @@ -16,6 +16,7 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif import { trackEvent } from '@/services/analytics' import useSafeInfo from '@/hooks/useSafeInfo' import CheckWallet from '@/components/common/CheckWallet' +import CloseIcon from '@/public/images/common/close.svg' import css from './styles.module.css' @@ -47,18 +48,12 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } })) }, [safe.chainId, setDismissedBannerPerChain]) - // Click outside to dismiss banner useEffect(() => { if (!shouldShowBanner) { return } trackEvent(PUSH_NOTIFICATION_EVENTS.DISPLAY_BANNER) - - document.addEventListener('click', dismissBanner) - return () => { - document.removeEventListener('click', dismissBanner) - } }, [dismissBanner, shouldShowBanner]) const onEnableAll = async () => { @@ -93,6 +88,9 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } Enable push notifications + + + Get notified about pending signatures, incoming and outgoing transactions and more when Safe{`{Wallet}`}{' '} is in the background or closed. diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css index 821b9b6cd8..f3e6c1ecd8 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/styles.module.css @@ -17,6 +17,13 @@ min-width: 100%; } +.close { + position: absolute; + top: var(--space-2); + right: var(--space-2); + color: var(--color-border-main); +} + .button { padding: 4px 10px; } From 28517d76bd97facce2aadc7d8023e01e3c57c70f Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 18 Sep 2023 18:16:51 +0200 Subject: [PATCH 51/62] fix: remove unnecessary types + qa findings --- .../GlobalPushNotifications.tsx | 18 +- .../PushNotificationsBanner/index.tsx | 96 +++- .../useNotificationPreferences.test.ts | 4 +- .../__tests__/useNotificationTracking.test.ts | 25 +- .../hooks/useNotificationPreferences.ts | 18 +- .../settings/PushNotifications/index.tsx | 40 +- .../__tests__/notifications.test.ts | 468 ++++++++---------- .../firebase-messaging/notification-mapper.ts | 57 +-- .../firebase-messaging/webhook-types.ts | 53 +- .../analytics/events/push-notifications.ts | 7 +- src/services/push-notifications/tracking.ts | 4 +- 11 files changed, 369 insertions(+), 421 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 6421615f1d..fbf1813b4c 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -1,4 +1,5 @@ import { + Box, Grid, Paper, Typography, @@ -24,6 +25,7 @@ import { selectAllAddedSafes } from '@/store/addedSafesSlice' import { trackEvent } from '@/services/analytics' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { requestNotificationPermission } from './logic' +import { useDismissPushNotificationsBanner } from './PushNotificationsBanner' import type { NotifiableSafes } from './logic' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' @@ -125,7 +127,7 @@ const getSafesToRegister = ( ) if (safesToRegisterOnChain.length > 0) { - acc[chainId] = safeAddresses + acc[chainId] = safesToRegisterOnChain } return acc @@ -154,7 +156,7 @@ const getSafesToUnregister = ( ) if (safesToUnregisterOnChain.length > 0) { - acc[chainId] = safeAddresses + acc[chainId] = safesToUnregisterOnChain } return acc }, @@ -191,6 +193,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { const chains = useChains() const addedSafes = useAppSelector(selectAllAddedSafes) + const { dismissPushNotificationBanner } = useDismissPushNotificationsBanner() const { getAllPreferences } = useNotificationPreferences() const { unregisterDeviceNotifications, unregisterSafeNotifications, registerNotifications } = useNotificationRegistrations() @@ -274,6 +277,11 @@ export const GlobalPushNotifications = (): ReactElement | null => { const safesToRegister = getSafesToRegister(selectedSafes, currentNotifiedSafes) if (safesToRegister) { registrationPromises.push(registerNotifications(safesToRegister)) + + // Dismiss the banner for all chains that have been registered + Object.keys(safesToRegister).forEach((chainId) => { + dismissPushNotificationBanner(chainId) + }) } const safesToUnregister = getSafesToUnregister(selectedSafes, currentNotifiedSafes) @@ -304,9 +312,9 @@ export const GlobalPushNotifications = (): ReactElement | null => { My Safes Accounts ({totalNotifiableSafes}) -
+ {totalSignaturesRequired > 0 && ( - + We'll ask you to verify your ownership of {totalSignaturesRequired} Safe Account {totalSignaturesRequired > 1 ? 's' : ''} with your signature @@ -319,7 +327,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { )} -
+ diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index ea1fe79a3d..e26287819e 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -17,10 +17,70 @@ import { trackEvent } from '@/services/analytics' import useSafeInfo from '@/hooks/useSafeInfo' import CheckWallet from '@/components/common/CheckWallet' import CloseIcon from '@/public/images/common/close.svg' +import type { AddedSafesState } from '@/store/addedSafesSlice' +import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' +import type { NotifiableSafes } from '../logic' import css from './styles.module.css' +import { useNotificationPreferences } from '../hooks/useNotificationPreferences' +import { sameAddress } from '@/utils/addresses' -const DISMISS_NOTIFICATION_KEY = 'dismissPushNotifications' +const DISMISS_PUSH_NOTIFICATIONS_KEY = 'dismissPushNotifications' + +export const useDismissPushNotificationsBanner = () => { + const addedSafes = useAppSelector(selectAllAddedSafes) + const { safe } = useSafeInfo() + + const [dismissedBannerPerChain = {}, setDismissedBannerPerChain] = useLocalStorage<{ + [chainId: string]: { [safeAddress: string]: boolean } + }>(DISMISS_PUSH_NOTIFICATIONS_KEY) + + const dismissPushNotificationBanner = (chainId: string) => { + const safesOnChain = Object.keys(addedSafes[chainId] || {}) + + if (safesOnChain.length === 0) { + return + } + + const dismissedSafesOnChain = safesOnChain.reduce<{ [safeAddress: string]: boolean }>((acc, safeAddress) => { + acc[safeAddress] = true + return acc + }, {}) + + setDismissedBannerPerChain((prev) => ({ + ...prev, + [safe.chainId]: dismissedSafesOnChain, + })) + } + + const isPushNotificationBannerDismissed = !!dismissedBannerPerChain[safe.chainId]?.[safe.address.value] + + return { + dismissPushNotificationBanner, + isPushNotificationBannerDismissed, + } +} + +const getSafesToRegister = (addedSafes: AddedSafesState, allPreferences: PushNotificationPreferences | undefined) => { + // Regiser all added Safes + if (!allPreferences) { + return transformAddedSafes(addedSafes) + } + + // Only register Safes that are not already registered + return Object.entries(addedSafes).reduce((acc, [chainId, addedSafesOnChain]) => { + const addedSafeAddressesOnChain = Object.keys(addedSafesOnChain) + const notificationRegistrations = Object.values(allPreferences) + + const newlyAddedSafes = addedSafeAddressesOnChain.filter((safeAddress) => { + return notificationRegistrations.some((registration) => !sameAddress(registration.safeAddress, safeAddress)) + }) + + acc[chainId] = newlyAddedSafes + + return acc + }, {}) +} export const PushNotificationsBanner = ({ children }: { children: ReactElement }): ReactElement => { const addedSafes = useAppSelector(selectAllAddedSafes) @@ -28,38 +88,31 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } const { safe } = useSafeInfo() const { query } = useRouter() - const [dismissedBannerPerChain = {}, setDismissedBannerPerChain] = useLocalStorage<{ - [chainId: string]: boolean - }>(DISMISS_NOTIFICATION_KEY) + const { dismissPushNotificationBanner, isPushNotificationBannerDismissed } = useDismissPushNotificationsBanner() const hasAddedSafesOnChain = Object.values(addedSafes[safe.chainId] || {}).length > 0 - const dismissedBanner = !!dismissedBannerPerChain[safe.chainId] - - const shouldShowBanner = !dismissedBanner && hasAddedSafesOnChain + const shouldShowBanner = !isPushNotificationBannerDismissed && hasAddedSafesOnChain const { registerNotifications } = useNotificationRegistrations() + const { getAllPreferences } = useNotificationPreferences() const dismissBanner = useCallback(() => { trackEvent(PUSH_NOTIFICATION_EVENTS.DISMISS_BANNER) - - setDismissedBannerPerChain((prev) => ({ - ...prev, - [safe.chainId]: true, - })) - }, [safe.chainId, setDismissedBannerPerChain]) + dismissPushNotificationBanner(safe.chainId) + }, [dismissPushNotificationBanner, safe.chainId]) useEffect(() => { - if (!shouldShowBanner) { - return + if (shouldShowBanner) { + trackEvent(PUSH_NOTIFICATION_EVENTS.DISPLAY_BANNER) } - - trackEvent(PUSH_NOTIFICATION_EVENTS.DISPLAY_BANNER) }, [dismissBanner, shouldShowBanner]) const onEnableAll = async () => { trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL) - const safesToRegister = transformAddedSafes(addedSafes) + const allPreferences = getAllPreferences() + const safesToRegister = getSafesToRegister(addedSafes, allPreferences) + await registerNotifications(safesToRegister) dismissBanner() @@ -112,12 +165,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } )} {safe && ( - + diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts index d17ca713df..043e610c51 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationPreferences.test.ts @@ -175,7 +175,7 @@ describe('useNotificationPreferences', () => { result.current.updatePreferences(chainId, safeAddress, { ...DEFAULT_NOTIFICATION_PREFERENCES, - [WebhookType.NEW_CONFIRMATION]: false, + [WebhookType.CONFIRMATION_REQUEST]: false, }) await waitFor(() => { @@ -185,7 +185,7 @@ describe('useNotificationPreferences', () => { safeAddress: safeAddress, preferences: { ...DEFAULT_NOTIFICATION_PREFERENCES, - [WebhookType.NEW_CONFIRMATION]: false, + [WebhookType.CONFIRMATION_REQUEST]: false, }, }, }) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts index c32a3897fd..c8601fb782 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts @@ -27,18 +27,10 @@ describe('useNotificationTracking', () => { shown: 1, opened: 0, }, - [`2:${WebhookType.OUTGOING_ETHER}`]: { - shown: 0, - opened: 1, - }, [`3:${WebhookType.INCOMING_TOKEN}`]: { shown: 1, opened: 1, }, - [`137:${WebhookType.OUTGOING_TOKEN}`]: { - shown: 0, - opened: 0, - }, } await setMany(Object.entries(cache), createNotificationTrackingIndexedDb()) @@ -46,7 +38,7 @@ describe('useNotificationTracking', () => { renderHook(() => useNotificationTracking()) await waitFor(() => { - expect(tracking.trackEvent).toHaveBeenCalledTimes(4) + expect(tracking.trackEvent).toHaveBeenCalledTimes(3) expect(tracking.trackEvent).toHaveBeenCalledWith({ ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, @@ -54,17 +46,12 @@ describe('useNotificationTracking', () => { chainId: '1', }) - expect(tracking.trackEvent).toHaveBeenCalledWith({ - ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, - label: WebhookType.OUTGOING_ETHER, - chainId: '2', - }) - expect(tracking.trackEvent).toHaveBeenCalledWith({ ...PUSH_NOTIFICATION_EVENTS.SHOW_NOTIFICATION, label: WebhookType.INCOMING_TOKEN, chainId: '3', }) + expect(tracking.trackEvent).toHaveBeenCalledWith({ ...PUSH_NOTIFICATION_EVENTS.OPEN_NOTIFICATION, label: WebhookType.INCOMING_TOKEN, @@ -78,18 +65,10 @@ describe('useNotificationTracking', () => { shown: 0, opened: 0, }, - [`2:${WebhookType.OUTGOING_ETHER}`]: { - shown: 0, - opened: 0, - }, [`3:${WebhookType.INCOMING_TOKEN}`]: { shown: 0, opened: 0, }, - [`137:${WebhookType.OUTGOING_TOKEN}`]: { - shown: 0, - opened: 0, - }, }) }) diff --git a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts index 8b2c7378f5..e74d6f4c03 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationPreferences.ts @@ -17,20 +17,22 @@ import { } from '@/services/push-notifications/preferences' import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' +import useIsSafeOwner from '@/hooks/useIsSafeOwner' import type { PushNotificationPreferences, PushNotificationPrefsKey } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' export const DEFAULT_NOTIFICATION_PREFERENCES: PushNotificationPreferences[PushNotificationPrefsKey]['preferences'] = { - [WebhookType.NEW_CONFIRMATION]: true, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: true, - [WebhookType.PENDING_MULTISIG_TRANSACTION]: true, [WebhookType.INCOMING_ETHER]: true, - [WebhookType.OUTGOING_ETHER]: true, [WebhookType.INCOMING_TOKEN]: true, - [WebhookType.OUTGOING_TOKEN]: true, [WebhookType.MODULE_TRANSACTION]: true, - [WebhookType.CONFIRMATION_REQUEST]: true, // Requires signature + [WebhookType.CONFIRMATION_REQUEST]: false, // Requires signature [WebhookType.SAFE_CREATED]: false, // We do not preemptively subscribe to Safes before they are created + // Disabled on the Transaction Service but kept here for completeness + [WebhookType._PENDING_MULTISIG_TRANSACTION]: true, + [WebhookType._NEW_CONFIRMATION]: true, + [WebhookType._OUTGOING_ETHER]: true, + [WebhookType._OUTGOING_TOKEN]: true, } // ExternalStores are used to keep indexedDB state synced across hook instances @@ -57,6 +59,7 @@ export const useNotificationPreferences = (): { // State const uuid = useUuid() const preferences = usePreferences() + const isOwner = useIsSafeOwner() // Getters const getPreferences = (chainId: string, safeAddress: string) => { @@ -149,7 +152,10 @@ export const useNotificationPreferences = (): { const defaultPreferences: PushNotificationPreferences[PushNotificationPrefsKey] = { chainId, safeAddress, - preferences: DEFAULT_NOTIFICATION_PREFERENCES, + preferences: { + ...DEFAULT_NOTIFICATION_PREFERENCES, + [WebhookType.CONFIRMATION_REQUEST]: isOwner, + }, } return [key, defaultPreferences] diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index 3a62d31590..7ea459bb60 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -27,6 +27,8 @@ import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notif import { AppRoutes } from '@/config/routes' import CheckWallet from '@/components/common/CheckWallet' import { useIsMac } from '@/hooks/useIsMac' +import useOnboard from '@/hooks/wallets/useOnboard' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import css from './styles.module.css' @@ -36,6 +38,7 @@ export const PushNotifications = (): ReactElement => { const isMac = useIsMac() const [isRegistering, setIsRegistering] = useState(false) const [isUpdatingIndexedDb, setIsUpdatingIndexedDb] = useState(false) + const onboard = useOnboard() const { updatePreferences, getPreferences, getAllPreferences } = useNotificationPreferences() const { unregisterSafeNotifications, unregisterDeviceNotifications, registerNotifications } = @@ -52,11 +55,16 @@ export const PushNotifications = (): ReactElement => { } const shouldShowMacHelper = isMac || IS_DEV - const isCheckboxDisabled = isRegistering || isUpdatingIndexedDb const handleOnChange = async () => { + if (!onboard) { + return + } + setIsRegistering(true) + await assertWalletChain(onboard, safe.chainId) + if (!preferences) { await registerNotifications({ [safe.chainId]: [safe.address.value] }) trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_SAFE) @@ -100,7 +108,7 @@ export const PushNotifications = (): ReactElement => { {shouldShowMacHelper && ( - For MacOS users + For macOS users Double-check that you have enabled your browser notifications under System Settings >{' '} @@ -121,7 +129,7 @@ export const PushNotifications = (): ReactElement => { showName={true} hasExplorer /> - + {(isOk) => ( } @@ -183,21 +191,15 @@ export const PushNotifications = (): ReactElement => { control={ { setPreferences({ ...preferences, - [WebhookType.OUTGOING_ETHER]: checked, - [WebhookType.OUTGOING_TOKEN]: checked, [WebhookType.MODULE_TRANSACTION]: checked, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: checked, - [WebhookType.PENDING_MULTISIG_TRANSACTION]: checked, }) trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_OUTGOING_TXS, label: checked }) @@ -207,24 +209,6 @@ export const PushNotifications = (): ReactElement => { label="Outgoing transactions" /> - { - setPreferences({ - ...preferences, - [WebhookType.NEW_CONFIRMATION]: checked, - }) - - trackEvent({ ...PUSH_NOTIFICATION_EVENTS.TOGGLE_NEW_CONFIRMATION, label: checked }) - }} - /> - } - label="New confirmations" - /> - { getBalancesMockSpy = jest.spyOn(sdk, 'getBalances') }) - describe('should parse NEW_CONFIRMATION payloads', () => { - const payload: NewConfirmationEvent = { - type: WebhookType.NEW_CONFIRMATION, - chainId: '1', - address: hexZeroPad('0x1', 20), - owner: hexZeroPad('0x2', 20), - safeTxHash: hexZeroPad('0x3', 32), - } - - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], - }) - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Transaction confirmation', - body: 'Safe 0x0000...0001 on Mainnet has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', - }) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Transaction confirmation', - body: 'Safe 0x0000...0001 on chain 1 has a new confirmation from 0x0000...0002 on transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) - }) - }) - describe('should parse EXECUTED_MULTISIG_TRANSACTION payloads', () => { const payload: Omit = { type: WebhookType.EXECUTED_MULTISIG_TRANSACTION, @@ -147,41 +111,6 @@ describe('parseWebhookPushNotification', () => { }) }) - describe('should parse PENDING_MULTISIG_TRANSACTION payloads', () => { - const payload: PendingMultisigTransactionEvent = { - type: WebhookType.PENDING_MULTISIG_TRANSACTION, - chainId: '1', - address: hexZeroPad('0x1', 20), - safeTxHash: hexZeroPad('0x3', 32), - } - - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], - }) - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Pending transaction', - body: 'Safe 0x0000...0001 on Mainnet has a pending transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', - }) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Pending transaction', - body: 'Safe 0x0000...0001 on chain 1 has a pending transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) - }) - }) - describe('should parse INCOMING_ETHER payloads', () => { const payload: IncomingEtherEvent = { type: WebhookType.INCOMING_ETHER, @@ -224,48 +153,6 @@ describe('parseWebhookPushNotification', () => { }) }) - describe('should parse OUTGOING_ETHER payloads', () => { - const payload: OutgoingEtherEvent = { - type: WebhookType.OUTGOING_ETHER, - chainId: '137', - address: hexZeroPad('0x1', 20), - txHash: hexZeroPad('0x3', 32), - value: '1000000000000000000', - } - - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [ - { - chainName: 'Polygon', - chainId: payload.chainId, - shortName: 'matic', - nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, - } as sdk.ChainInfo, - ], - }) - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Matic sent', - body: 'Safe 0x0000...0001 on Polygon sent 1.0 MATIC in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=matic:0x0000000000000000000000000000000000000001', - }) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Ether sent', - body: 'Safe 0x0000...0001 on chain 137 sent 1.0 ETH in transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) - }) - }) - describe('should parse INCOMING_TOKEN payloads', () => { const payload: IncomingTokenEvent = { type: WebhookType.INCOMING_TOKEN, @@ -429,9 +316,201 @@ describe('parseWebhookPushNotification', () => { }) }) - describe('should parse OUTGOING_TOKEN payloads', () => { + describe('should parse MODULE_TRANSACTION payloads', () => { + const payload: ModuleTransactionEvent = { + type: WebhookType.MODULE_TRANSACTION, + chainId: '1', + address: hexZeroPad('0x1', 20), + module: hexZeroPad('0x2', 20), + txHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual({ + title: 'Module transaction', + body: 'Safe 0x0000...0001 on Mainnet executed a module transaction 0x0000...0003 from module 0x0000...0002.', + link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', + }) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual({ + title: 'Module transaction', + body: 'Safe 0x0000...0001 on chain 1 executed a module transaction 0x0000...0003 from module 0x0000...0002.', + link: 'https://app.safe.global', + }) + }) + }) + + describe('should parse CONFIRMATION_REQUEST payloads', () => { + const payload: ConfirmationRequestEvent = { + type: WebhookType.CONFIRMATION_REQUEST, + chainId: '1', + address: hexZeroPad('0x1', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual({ + title: 'Confirmation request', + body: 'Safe 0x0000...0001 on Mainnet has a new confirmation request for transaction 0x0000...0003.', + link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', + }) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual({ + title: 'Confirmation request', + body: 'Safe 0x0000...0001 on chain 1 has a new confirmation request for transaction 0x0000...0003.', + link: 'https://app.safe.global', + }) + }) + }) + + // We do not pre-emptively subscribe to Safes before they are created + describe('should not parse SAFE_CREATED payloads', () => { + const payload: SafeCreatedEvent = { + type: WebhookType.SAFE_CREATED, + chainId: '1', + address: hexZeroPad('0x1', 20), + txHash: hexZeroPad('0x3', 32), + blockNumber: '1', + } + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], + }) + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toBe(undefined) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toBe(undefined) + }) + }) + + // Not enabled in the Transaction Service + describe('should not parse NEW_CONFIRMATION payloads', () => { + const payload: NewConfirmationEvent = { + type: WebhookType._NEW_CONFIRMATION, + chainId: '1', + address: hexZeroPad('0x1', 20), + owner: hexZeroPad('0x2', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + }) + + // Not enabled in the Transaction Service + describe('should not parse PENDING_MULTISIG_TRANSACTION payloads', () => { + const payload: PendingMultisigTransactionEvent = { + type: WebhookType._PENDING_MULTISIG_TRANSACTION, + chainId: '1', + address: hexZeroPad('0x1', 20), + safeTxHash: hexZeroPad('0x3', 32), + } + + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], + }) + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + }) + + // Not enabled in the Transaction Service + describe('should not parse OUTGOING_ETHER payloads', () => { + const payload: OutgoingEtherEvent = { + type: WebhookType._OUTGOING_ETHER, + chainId: '137', + address: hexZeroPad('0x1', 20), + txHash: hexZeroPad('0x3', 32), + value: '1000000000000000000', + } + + it('with chain info', async () => { + getChainsConfigSpy.mockResolvedValue({ + results: [ + { + chainName: 'Polygon', + chainId: payload.chainId, + shortName: 'matic', + nativeCurrency: { name: 'Matic', symbol: 'MATIC', decimals: 18 }, + } as sdk.ChainInfo, + ], + }) + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + + it('without chain info', async () => { + getChainsConfigSpy.mockImplementationOnce(() => Promise.reject()) // chains + + const notification = await _parseServiceWorkerWebhookPushNotification(payload) + + expect(notification).toEqual(undefined) + }) + }) + + // Not enabled in the Transaction Service + describe('should not parse OUTGOING_TOKEN payloads', () => { const payload: OutgoingTokenEvent = { - type: WebhookType.OUTGOING_TOKEN, + type: WebhookType._OUTGOING_TOKEN, chainId: '1', address: hexZeroPad('0x1', 20), tokenAddress: hexZeroPad('0x2', 20), @@ -462,11 +541,7 @@ describe('parseWebhookPushNotification', () => { const notification = await _parseServiceWorkerWebhookPushNotification(payload) - expect(notification).toEqual({ - title: 'Fake sent', - body: 'Safe 0x0000...0001 on Mainnet sent some FAKE in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(notification).toEqual(undefined) getChainsConfigSpy.mockResolvedValue({ results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], @@ -486,11 +561,7 @@ describe('parseWebhookPushNotification', () => { const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) - expect(erc20Notification).toEqual({ - title: 'Fake sent', - body: 'Safe 0x0000...0001 on Mainnet sent 1.0 FAKE in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(erc20Notification).toEqual(undefined) }) it('with chain and empty token info', async () => { @@ -503,11 +574,7 @@ describe('parseWebhookPushNotification', () => { const notification = await _parseServiceWorkerWebhookPushNotification(payload) - expect(notification).toEqual({ - title: 'Token sent', - body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(notification).toEqual(undefined) getChainsConfigSpy.mockResolvedValue({ results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], @@ -527,11 +594,7 @@ describe('parseWebhookPushNotification', () => { const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) - expect(erc20Notification).toEqual({ - title: 'Fake sent', - body: 'Safe 0x0000...0001 on Mainnet sent 1.0 FAKE in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(erc20Notification).toEqual(undefined) }) it('without chain info', async () => { @@ -551,11 +614,7 @@ describe('parseWebhookPushNotification', () => { const notification = await _parseServiceWorkerWebhookPushNotification(payload) - expect(notification).toEqual({ - title: 'Fake sent', - body: 'Safe 0x0000...0001 on chain 1 sent some FAKE in transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) + expect(notification).toEqual(undefined) getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockResolvedValue({ @@ -573,12 +632,9 @@ describe('parseWebhookPushNotification', () => { const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) - expect(erc20Notification).toEqual({ - title: 'Fake sent', - body: 'Safe 0x0000...0001 on chain 1 sent 1.0 FAKE in transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) + expect(erc20Notification).toEqual(undefined) }) + it('without token info', async () => { getChainsConfigSpy.mockResolvedValue({ results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], @@ -587,11 +643,7 @@ describe('parseWebhookPushNotification', () => { const notification = await _parseServiceWorkerWebhookPushNotification(payload) - expect(notification).toEqual({ - title: 'Token sent', - body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(notification).toEqual(undefined) getChainsConfigSpy.mockResolvedValue({ results: [{ chainName: 'Mainnet', chainId: payload.chainId, shortName: 'eth' } as sdk.ChainInfo], @@ -600,11 +652,7 @@ describe('parseWebhookPushNotification', () => { const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) - expect(erc20Notification).toEqual({ - title: 'Token sent', - body: 'Safe 0x0000...0001 on Mainnet sent some tokens in transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) + expect(erc20Notification).toEqual(undefined) }) it('without chain and balance info', async () => { @@ -613,120 +661,14 @@ describe('parseWebhookPushNotification', () => { const notification = await _parseServiceWorkerWebhookPushNotification(payload) - expect(notification).toEqual({ - title: 'Token sent', - body: 'Safe 0x0000...0001 on chain 1 sent some tokens in transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) + expect(notification).toEqual(undefined) getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains getBalancesMockSpy.mockImplementation(() => Promise.reject()) // tokens const erc20Notification = await _parseServiceWorkerWebhookPushNotification(erc20Payload) - expect(erc20Notification).toEqual({ - title: 'Token sent', - body: 'Safe 0x0000...0001 on chain 1 sent some tokens in transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) - }) - }) - - describe('should parse MODULE_TRANSACTION payloads', () => { - const payload: ModuleTransactionEvent = { - type: WebhookType.MODULE_TRANSACTION, - chainId: '1', - address: hexZeroPad('0x1', 20), - module: hexZeroPad('0x2', 20), - txHash: hexZeroPad('0x3', 32), - } - - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], - }) - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Module transaction', - body: 'Safe 0x0000...0001 on Mainnet executed a module transaction 0x0000...0003 from module 0x0000...0002.', - link: 'https://app.safe.global/transactions/history?safe=eth:0x0000000000000000000000000000000000000001', - }) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Module transaction', - body: 'Safe 0x0000...0001 on chain 1 executed a module transaction 0x0000...0003 from module 0x0000...0002.', - link: 'https://app.safe.global', - }) - }) - }) - - describe('should parse CONFIRMATION_REQUEST payloads', () => { - const payload: ConfirmationRequestEvent = { - type: WebhookType.CONFIRMATION_REQUEST, - chainId: '1', - address: hexZeroPad('0x1', 20), - safeTxHash: hexZeroPad('0x3', 32), - } - - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], - }) - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Confirmation request', - body: 'Safe 0x0000...0001 on Mainnet has a new confirmation request for transaction 0x0000...0003.', - link: 'https://app.safe.global/transactions/tx?safe=eth:0x0000000000000000000000000000000000000001&id=0x0000000000000000000000000000000000000000000000000000000000000003', - }) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toEqual({ - title: 'Confirmation request', - body: 'Safe 0x0000...0001 on chain 1 has a new confirmation request for transaction 0x0000...0003.', - link: 'https://app.safe.global', - }) - }) - }) - - describe('should not parse SAFE_CREATED payloads', () => { - const payload: SafeCreatedEvent = { - type: WebhookType.SAFE_CREATED, - chainId: '1', - address: hexZeroPad('0x1', 20), - txHash: hexZeroPad('0x3', 32), - blockNumber: '1', - } - it('with chain info', async () => { - getChainsConfigSpy.mockResolvedValue({ - results: [{ chainName: 'Mainnet', chainId: '1', shortName: 'eth' } as sdk.ChainInfo], - }) - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toBe(undefined) - }) - - it('without chain info', async () => { - getChainsConfigSpy.mockImplementation(() => Promise.reject()) // chains - - const notification = await _parseServiceWorkerWebhookPushNotification(payload) - - expect(notification).toBe(undefined) + expect(erc20Notification).toEqual(undefined) }) }) }) diff --git a/src/service-workers/firebase-messaging/notification-mapper.ts b/src/service-workers/firebase-messaging/notification-mapper.ts index ad392908fe..e5c8773ab0 100644 --- a/src/service-workers/firebase-messaging/notification-mapper.ts +++ b/src/service-workers/firebase-messaging/notification-mapper.ts @@ -73,15 +73,6 @@ const shortenAddress = (address: string, length = 4): string => { } export const Notifications: PushNotificationsMap = { - [WebhookType.NEW_CONFIRMATION]: ({ address, owner, safeTxHash, chainId }, chain) => { - return { - title: 'Transaction confirmation', - body: `Safe ${shortenAddress(address)} on ${getChainName( - chainId, - chain, - )} has a new confirmation from ${shortenAddress(owner)} on transaction ${shortenAddress(safeTxHash)}.`, - } - }, [WebhookType.EXECUTED_MULTISIG_TRANSACTION]: ({ address, failed, txHash, chainId }, chain) => { const didFail = failed === 'true' return { @@ -91,15 +82,6 @@ export const Notifications: PushNotificationsMap = { } transaction ${shortenAddress(txHash)}.`, } }, - [WebhookType.PENDING_MULTISIG_TRANSACTION]: ({ address, safeTxHash, chainId }, chain) => { - return { - title: 'Pending transaction', - body: `Safe ${shortenAddress(address)} on ${getChainName( - chainId, - chain, - )} has a pending transaction ${shortenAddress(safeTxHash)}.`, - } - }, [WebhookType.INCOMING_ETHER]: ({ address, txHash, value, chainId }, chain) => { return { title: `${getCurrencyName(chain)} received`, @@ -109,15 +91,6 @@ export const Notifications: PushNotificationsMap = { ).toString()} ${getCurrencySymbol(chain)} in transaction ${shortenAddress(txHash)}.`, } }, - [WebhookType.OUTGOING_ETHER]: ({ address, txHash, value, chainId }, chain) => { - return { - title: `${getCurrencyName(chain)} sent`, - body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} sent ${formatUnits( - value, - chain?.nativeCurrency?.decimals, - ).toString()} ${getCurrencySymbol(chain)} in transaction ${shortenAddress(txHash)}.`, - } - }, [WebhookType.INCOMING_TOKEN]: async ({ address, txHash, tokenAddress, value, chainId }, chain) => { const token = await getTokenInfo(chainId, address, tokenAddress, value) return { @@ -127,15 +100,6 @@ export const Notifications: PushNotificationsMap = { } in transaction ${shortenAddress(txHash)}.`, } }, - [WebhookType.OUTGOING_TOKEN]: async ({ address, txHash, tokenAddress, value, chainId }, chain) => { - const token = await getTokenInfo(chainId, address, tokenAddress, value) - return { - title: `${token.name} sent`, - body: `Safe ${shortenAddress(address)} on ${getChainName(chainId, chain)} sent ${token.value} ${ - token.symbol - } in transaction ${shortenAddress(txHash)}.`, - } - }, [WebhookType.MODULE_TRANSACTION]: ({ address, module, txHash, chainId }, chain) => { return { title: 'Module transaction', @@ -158,4 +122,25 @@ export const Notifications: PushNotificationsMap = { // We do not preemptively subscribe to Safes before they are created return null }, + // Disabled on the Transaction Service + [WebhookType._PENDING_MULTISIG_TRANSACTION]: () => { + // We don't send notifications for pending transactions + // @see https://github.com/safe-global/safe-transaction-service/blob/master/safe_transaction_service/notifications/tasks.py#L34 + return null + }, + [WebhookType._NEW_CONFIRMATION]: () => { + // Disabled for now + // @see https://github.com/safe-global/safe-transaction-service/blob/master/safe_transaction_service/notifications/tasks.py#L43 + return null + }, + [WebhookType._OUTGOING_TOKEN]: () => { + // We don't sen as we have execution notifications + // @see https://github.com/safe-global/safe-transaction-service/blob/master/safe_transaction_service/notifications/tasks.py#L48 + return null + }, + [WebhookType._OUTGOING_ETHER]: () => { + // We don't sen as we have execution notifications + // @see https://github.com/safe-global/safe-transaction-service/blob/master/safe_transaction_service/notifications/tasks.py#L48 + return null + }, } diff --git a/src/service-workers/firebase-messaging/webhook-types.ts b/src/service-workers/firebase-messaging/webhook-types.ts index 3f1052e4f6..c774a0f60a 100644 --- a/src/service-workers/firebase-messaging/webhook-types.ts +++ b/src/service-workers/firebase-messaging/webhook-types.ts @@ -7,69 +7,70 @@ export const isWebhookEvent = (data: MessagePayload['data']): data is WebhookEve } export enum WebhookType { - NEW_CONFIRMATION = 'NEW_CONFIRMATION', EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', - PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', INCOMING_ETHER = 'INCOMING_ETHER', - OUTGOING_ETHER = 'OUTGOING_ETHER', INCOMING_TOKEN = 'INCOMING_TOKEN', - OUTGOING_TOKEN = 'OUTGOING_TOKEN', MODULE_TRANSACTION = 'MODULE_TRANSACTION', CONFIRMATION_REQUEST = 'CONFIRMATION_REQUEST', // Notification-specific webhook SAFE_CREATED = 'SAFE_CREATED', + // Disabled on the Transaction Service + _PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', + _NEW_CONFIRMATION = 'NEW_CONFIRMATION', + _OUTGOING_ETHER = 'OUTGOING_ETHER', + _OUTGOING_TOKEN = 'OUTGOING_TOKEN', } -export type NewConfirmationEvent = { - type: WebhookType.NEW_CONFIRMATION +export type PendingMultisigTransactionEvent = { + type: WebhookType._PENDING_MULTISIG_TRANSACTION chainId: string address: string - owner: string safeTxHash: string } -export type ExecutedMultisigTransactionEvent = { - type: WebhookType.EXECUTED_MULTISIG_TRANSACTION +export type NewConfirmationEvent = { + type: WebhookType._NEW_CONFIRMATION chainId: string address: string + owner: string safeTxHash: string - failed: 'true' | 'false' - txHash: string } -export type PendingMultisigTransactionEvent = { - type: WebhookType.PENDING_MULTISIG_TRANSACTION +export type OutgoingEtherEvent = { + type: WebhookType._OUTGOING_ETHER chainId: string address: string - safeTxHash: string + txHash: string + value: string } -export type IncomingEtherEvent = { - type: WebhookType.INCOMING_ETHER +export type OutgoingTokenEvent = { + type: WebhookType._OUTGOING_TOKEN chainId: string address: string + tokenAddress: string txHash: string - value: string + value?: string // If ERC-20 token } -export type OutgoingEtherEvent = { - type: WebhookType.OUTGOING_ETHER +export type ExecutedMultisigTransactionEvent = { + type: WebhookType.EXECUTED_MULTISIG_TRANSACTION chainId: string address: string + safeTxHash: string + failed: 'true' | 'false' txHash: string - value: string } -export type IncomingTokenEvent = { - type: WebhookType.INCOMING_TOKEN +export type IncomingEtherEvent = { + type: WebhookType.INCOMING_ETHER chainId: string address: string - tokenAddress: string txHash: string - value?: string // If ERC-20 token + value: string } -export type OutgoingTokenEvent = { - type: WebhookType.OUTGOING_TOKEN +export type IncomingTokenEvent = { + type: WebhookType.INCOMING_TOKEN chainId: string address: string tokenAddress: string diff --git a/src/services/analytics/events/push-notifications.ts b/src/services/analytics/events/push-notifications.ts index f401f7dee6..08e6a89a3b 100644 --- a/src/services/analytics/events/push-notifications.ts +++ b/src/services/analytics/events/push-notifications.ts @@ -68,16 +68,11 @@ export const PUSH_NOTIFICATION_EVENTS = { category, }, // User changed the outgoing transactions notifications setting - // (outgoing native currency/tokens, module/executed/pending multisig transactions) + // (module/executed multisig transactions) TOGGLE_OUTGOING_TXS: { action: 'Toggle outgoing assets notifications', category, }, - // User changed the new confirmation notifications setting - TOGGLE_NEW_CONFIRMATION: { - action: 'Toggle new confirmation notifications', - category, - }, // User changed the confirmation request notifications setting TOGGLE_CONFIRMATION_REQUEST: { action: 'Toggle confirmation request notifications', diff --git a/src/services/push-notifications/tracking.ts b/src/services/push-notifications/tracking.ts index 6d9755406e..3e695bfd4e 100644 --- a/src/services/push-notifications/tracking.ts +++ b/src/services/push-notifications/tracking.ts @@ -19,9 +19,9 @@ export const getNotificationTrackingKey = (chainId: string, type: WebhookType): } export const parseNotificationTrackingKey = (key: string): { chainId: string; type: WebhookType } => { - const [chainId, type] = key.split(':') + const [chainId, type] = key.split(':') as [string, WebhookType] - if (!Object.keys(WebhookType).includes(type)) { + if (!Object.values(WebhookType).includes(type)) { throw new Error(`Invalid notification tracking key: ${key}`) } From 8cea4ede52a78e83ab98a85ac7badcf3a822fc00 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 19 Sep 2023 13:11:55 +0200 Subject: [PATCH 52/62] fix: texts, confirmation registration + enable all --- .../GlobalPushNotifications.tsx | 4 ++-- .../PushNotificationsBanner/index.tsx | 20 +++++++++++++++---- .../settings/PushNotifications/index.tsx | 2 ++ .../settings/PushNotifications/logic.ts | 19 +++++++++++++----- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index fbf1813b4c..6429fc8a19 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -315,8 +315,8 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( - We'll ask you to verify your ownership of {totalSignaturesRequired} Safe Account - {totalSignaturesRequired > 1 ? 's' : ''} with your signature + We'll ask you to verify ownership of each Safe Account with your signature per chain + {totalSignaturesRequired} time{totalSignaturesRequired > 1 ? 's' : ''} )} diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index e26287819e..7aaaccf2ce 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -17,13 +17,15 @@ import { trackEvent } from '@/services/analytics' import useSafeInfo from '@/hooks/useSafeInfo' import CheckWallet from '@/components/common/CheckWallet' import CloseIcon from '@/public/images/common/close.svg' +import { useNotificationPreferences } from '../hooks/useNotificationPreferences' +import { sameAddress } from '@/utils/addresses' +import useOnboard from '@/hooks/wallets/useOnboard' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' import css from './styles.module.css' -import { useNotificationPreferences } from '../hooks/useNotificationPreferences' -import { sameAddress } from '@/utils/addresses' const DISMISS_PUSH_NOTIFICATIONS_KEY = 'dismissPushNotifications' @@ -73,7 +75,10 @@ const getSafesToRegister = (addedSafes: AddedSafesState, allPreferences: PushNot const notificationRegistrations = Object.values(allPreferences) const newlyAddedSafes = addedSafeAddressesOnChain.filter((safeAddress) => { - return notificationRegistrations.some((registration) => !sameAddress(registration.safeAddress, safeAddress)) + return ( + notificationRegistrations.length === 0 || + notificationRegistrations.some((registration) => !sameAddress(registration.safeAddress, safeAddress)) + ) }) acc[chainId] = newlyAddedSafes @@ -87,6 +92,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } const totalAddedSafes = useAppSelector(selectTotalAdded) const { safe } = useSafeInfo() const { query } = useRouter() + const onboard = useOnboard() const { dismissPushNotificationBanner, isPushNotificationBannerDismissed } = useDismissPushNotificationsBanner() @@ -108,11 +114,17 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } }, [dismissBanner, shouldShowBanner]) const onEnableAll = async () => { + if (!onboard) { + return + } + trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL) const allPreferences = getAllPreferences() const safesToRegister = getSafesToRegister(addedSafes, allPreferences) + await assertWalletChain(onboard, safe.chainId) + await registerNotifications(safesToRegister) dismissBanner() @@ -157,7 +169,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } size="small" className={css.button} onClick={onEnableAll} - disabled={!isOk} + disabled={!isOk || !onboard} > Enable all diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index 7ea459bb60..b845277fd2 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -105,6 +105,8 @@ export const PushNotifications = (): ReactElement => { with your signature. You will need to enable them again if you clear your browser cache. + {!safeLoaded && palette.primary.light}>No Safes added} + {shouldShowMacHelper && ( diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index d6792b7fd7..a37be46d7c 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -1,4 +1,4 @@ -import { keccak256, toUtf8Bytes } from 'ethers/lib/utils' +import { arrayify, keccak256, toUtf8Bytes } from 'ethers/lib/utils' import { getToken, getMessaging } from 'firebase/messaging' import { DeviceType } from '@safe-global/safe-gateway-typescript-sdk' import type { RegisterNotificationsRequest } from '@safe-global/safe-gateway-typescript-sdk' @@ -8,6 +8,7 @@ import { FIREBASE_VAPID_KEY, initializeFirebaseApp } from '@/services/push-notif import packageJson from '../../../../package.json' import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' +import { checksumAddress } from '@/utils/addresses' type WithRequired = T & { [P in K]-?: T[P] } @@ -51,10 +52,11 @@ const getSafeRegistrationSignature = ({ // @see https://github.com/safe-global/safe-transaction-service/blob/3644c08ac4b01b6a1c862567bc1d1c81b1a8c21f/safe_transaction_service/notifications/views.py#L19-L24 - const message = MESSAGE_PREFIX + timestamp + uuid + token + safeAddresses.join('') + const message = MESSAGE_PREFIX + timestamp + uuid + token + safeAddresses.sort().join('') const hashedMessage = keccak256(toUtf8Bytes(message)) - return web3.getSigner().signMessage(hashedMessage) + // TODO: Use `signMessage` when supported + return web3.getSigner()._legacySignMessage(arrayify(hashedMessage)) } export type NotifiableSafes = { [chainId: string]: Array } @@ -92,12 +94,19 @@ export const getRegisterDevicePayload = async ({ const safeRegistrations = await Promise.all( Object.entries(safesToRegister).map(async ([chainId, safeAddresses]) => { + const checksummedSafeAddresses = safeAddresses.map((address) => checksumAddress(address)) // We require a signature for confirmation request notifications - const signature = await getSafeRegistrationSignature({ safeAddresses, web3, uuid, timestamp, token }) + const signature = await getSafeRegistrationSignature({ + safeAddresses: checksummedSafeAddresses, + web3, + uuid, + timestamp, + token, + }) return { chainId, - safes: safeAddresses, + safes: checksummedSafeAddresses, signatures: [signature], } }), From 944bad857d8e46beebf6e74c24f6ae69ed10d138 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 19 Sep 2023 13:24:12 +0200 Subject: [PATCH 53/62] fix: mock --- src/components/settings/PushNotifications/logic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts index e217ef05cd..d443039a66 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/logic.test.ts @@ -99,7 +99,7 @@ describe('Notifications', () => { jest.spyOn(mockProvider, 'getSigner').mockImplementation( () => ({ - signMessage: jest.fn().mockResolvedValueOnce(signature), + _legacySignMessage: jest.fn().mockResolvedValueOnce(signature), } as unknown as JsonRpcSigner), ) From 124827dcbc2f67c50f74a8fe74b902677e2e1cf1 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 19 Sep 2023 14:54:54 +0200 Subject: [PATCH 54/62] fix: spacing --- .../settings/PushNotifications/GlobalPushNotifications.tsx | 4 ++-- src/components/settings/PushNotifications/index.tsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 6429fc8a19..56ee41ba4c 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -302,7 +302,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { } if (totalNotifiableSafes === 0) { - return null + return palette.primary.light}>No Safes added } return ( @@ -316,7 +316,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( We'll ask you to verify ownership of each Safe Account with your signature per chain - {totalSignaturesRequired} time{totalSignaturesRequired > 1 ? 's' : ''} + {totalSignaturesRequired} time {totalSignaturesRequired > 1 ? 's' : ''} )} diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index b845277fd2..7ea459bb60 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -105,8 +105,6 @@ export const PushNotifications = (): ReactElement => { with your signature. You will need to enable them again if you clear your browser cache. - {!safeLoaded && palette.primary.light}>No Safes added} - {shouldShowMacHelper && ( From e0bd7b8ed606cb795a63ed0f4c61d74283af7bfa Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 19 Sep 2023 14:59:40 +0200 Subject: [PATCH 55/62] fix: spacing --- .../settings/PushNotifications/GlobalPushNotifications.tsx | 4 ++-- src/components/settings/PushNotifications/index.tsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 6429fc8a19..56ee41ba4c 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -302,7 +302,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { } if (totalNotifiableSafes === 0) { - return null + return palette.primary.light}>No Safes added } return ( @@ -316,7 +316,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( We'll ask you to verify ownership of each Safe Account with your signature per chain - {totalSignaturesRequired} time{totalSignaturesRequired > 1 ? 's' : ''} + {totalSignaturesRequired} time {totalSignaturesRequired > 1 ? 's' : ''} )} diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index b845277fd2..7ea459bb60 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -105,8 +105,6 @@ export const PushNotifications = (): ReactElement => { with your signature. You will need to enable them again if you clear your browser cache. - {!safeLoaded && palette.primary.light}>No Safes added} - {shouldShowMacHelper && ( From e9e94c0250411ec18748c6e768654e73c3c0e502 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 22 Sep 2023 16:07:45 +0200 Subject: [PATCH 56/62] fix: use EIP-191 --- src/components/settings/PushNotifications/logic.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/settings/PushNotifications/logic.ts b/src/components/settings/PushNotifications/logic.ts index a37be46d7c..74c39b12d9 100644 --- a/src/components/settings/PushNotifications/logic.ts +++ b/src/components/settings/PushNotifications/logic.ts @@ -55,8 +55,7 @@ const getSafeRegistrationSignature = ({ const message = MESSAGE_PREFIX + timestamp + uuid + token + safeAddresses.sort().join('') const hashedMessage = keccak256(toUtf8Bytes(message)) - // TODO: Use `signMessage` when supported - return web3.getSigner()._legacySignMessage(arrayify(hashedMessage)) + return web3.getSigner().signMessage(arrayify(hashedMessage)) } export type NotifiableSafes = { [chainId: string]: Array } From 75b61778af55bb88253390e2fcd8cdd8bd6b33dd Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 09:42:56 +0200 Subject: [PATCH 57/62] fix: test --- src/components/settings/PushNotifications/logic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/PushNotifications/logic.test.ts b/src/components/settings/PushNotifications/logic.test.ts index d443039a66..e217ef05cd 100644 --- a/src/components/settings/PushNotifications/logic.test.ts +++ b/src/components/settings/PushNotifications/logic.test.ts @@ -99,7 +99,7 @@ describe('Notifications', () => { jest.spyOn(mockProvider, 'getSigner').mockImplementation( () => ({ - _legacySignMessage: jest.fn().mockResolvedValueOnce(signature), + signMessage: jest.fn().mockResolvedValueOnce(signature), } as unknown as JsonRpcSigner), ) From 21bdb3d49f84dbafdafd92cc3ecd1ed7f2462761 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 10:29:14 +0200 Subject: [PATCH 58/62] feat: add feature flag --- .../PushNotificationsBanner/index.tsx | 5 ++++- .../hooks/useNotificationTracking.ts | 8 ++++++-- src/components/settings/SettingsHeader/index.tsx | 11 ++++++++++- src/pages/settings/notifications.tsx | 8 ++++++++ src/utils/chains.ts | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index 7aaaccf2ce..ac02870bb1 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -21,6 +21,8 @@ import { useNotificationPreferences } from '../hooks/useNotificationPreferences' import { sameAddress } from '@/utils/addresses' import useOnboard from '@/hooks/wallets/useOnboard' import { assertWalletChain } from '@/services/tx/tx-sender/sdk' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' import type { AddedSafesState } from '@/store/addedSafesSlice' import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' @@ -88,6 +90,7 @@ const getSafesToRegister = (addedSafes: AddedSafesState, allPreferences: PushNot } export const PushNotificationsBanner = ({ children }: { children: ReactElement }): ReactElement => { + const isNotificationsEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const addedSafes = useAppSelector(selectAllAddedSafes) const totalAddedSafes = useAppSelector(selectTotalAdded) const { safe } = useSafeInfo() @@ -136,7 +139,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } dismissBanner() } - if (!shouldShowBanner) { + if (!shouldShowBanner || !isNotificationsEnabled) { return children } diff --git a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts index 773d9bc4df..5c0500422e 100644 --- a/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts +++ b/src/components/settings/PushNotifications/hooks/useNotificationTracking.ts @@ -12,6 +12,8 @@ import ErrorCodes from '@/services/exceptions/ErrorCodes' import { logError } from '@/services/exceptions' import type { NotificationTracking, NotificationTrackingKey } from '@/services/push-notifications/tracking' import type { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const trackNotificationEvents = ( chainId: string, @@ -68,9 +70,11 @@ const handleTrackCachedNotificationEvents = async ( } export const useNotificationTracking = (): void => { + const isNotificationsEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) + useEffect(() => { - if (typeof indexedDB !== 'undefined') { + if (typeof indexedDB !== 'undefined' && isNotificationsEnabled) { handleTrackCachedNotificationEvents(createNotificationTrackingIndexedDb()) } - }, []) + }, [isNotificationsEnabled]) } diff --git a/src/components/settings/SettingsHeader/index.tsx b/src/components/settings/SettingsHeader/index.tsx index 9efcd616d7..63f3e54be8 100644 --- a/src/components/settings/SettingsHeader/index.tsx +++ b/src/components/settings/SettingsHeader/index.tsx @@ -5,16 +5,25 @@ import PageHeader from '@/components/common/PageHeader' import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' import useSafeAddress from '@/hooks/useSafeAddress' +import { AppRoutes } from '@/config/routes' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const SettingsHeader = (): ReactElement => { const safeAddress = useSafeAddress() + const isNotificationsEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) + + const navItems = safeAddress ? settingsNavItems : generalSettingsNavItems + const filteredNavItems = isNotificationsEnabled + ? navItems + : navItems.filter((item) => item.href !== AppRoutes.settings.notifications) return ( - +
} /> diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx index 21d1059b9f..a739f3d5f4 100644 --- a/src/pages/settings/notifications.tsx +++ b/src/pages/settings/notifications.tsx @@ -3,8 +3,16 @@ import type { NextPage } from 'next' import SettingsHeader from '@/components/settings/SettingsHeader' import { PushNotifications } from '@/components/settings/PushNotifications' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const NotificationsPage: NextPage = () => { + const isNotificationsEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) + + if (!isNotificationsEnabled) { + return null + } + return ( <> diff --git a/src/utils/chains.ts b/src/utils/chains.ts index bbecb4d5f4..7ab92eed6c 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -14,6 +14,7 @@ export enum FEATURES { RELAYING = 'RELAYING', EIP1271 = 'EIP1271', RISK_MITIGATION = 'RISK_MITIGATION', + PUSH_NOTIFICATIONS = 'PUSH_NOTIFICATIONS', } export const hasFeature = (chain: ChainInfo, feature: FEATURES): boolean => { From 8c2ae58ed199094db72ab88552ef43a2b2a7b00b Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 11:13:01 +0200 Subject: [PATCH 59/62] fix: test --- .../hooks/__tests__/useNotificationTracking.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts index c8601fb782..1e8e834ea0 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationTracking.test.ts @@ -2,6 +2,7 @@ import 'fake-indexeddb/auto' import { entries, setMany } from 'idb-keyval' import * as tracking from '@/services/analytics' +import * as useChains from '@/hooks/useChains' import { PUSH_NOTIFICATION_EVENTS } from '@/services/analytics/events/push-notifications' import { createNotificationTrackingIndexedDb } from '@/services/push-notifications/tracking' import { WebhookType } from '@/service-workers/firebase-messaging/webhook-types' @@ -19,7 +20,17 @@ describe('useNotificationTracking', () => { jest.clearAllMocks() }) + it('should not track if the feature flag is disabled', async () => { + jest.spyOn(useChains, 'useHasFeature').mockReturnValue(false) + jest.spyOn(tracking, 'trackEvent') + + renderHook(() => useNotificationTracking()) + + expect(tracking.trackEvent).not.toHaveBeenCalled() + }) + it('should track all cached events and clear the cache', async () => { + jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(tracking, 'trackEvent') const cache = { @@ -73,6 +84,7 @@ describe('useNotificationTracking', () => { }) it('should not track if no cache exists', async () => { + jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(tracking, 'trackEvent') const _entries = await entries(createNotificationTrackingIndexedDb()) From e18c16b58c4791506e65014cb0e770d8e12a8072 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 16:22:54 +0200 Subject: [PATCH 60/62] fix: add space + only show banner on added Safes --- .../PushNotifications/GlobalPushNotifications.tsx | 2 +- .../PushNotifications/PushNotificationsBanner/index.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 56ee41ba4c..0c13531bb6 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -315,7 +315,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( - We'll ask you to verify ownership of each Safe Account with your signature per chain + We'll ask you to verify ownership of each Safe Account with your signature per chain{' '} {totalSignaturesRequired} time {totalSignaturesRequired > 1 ? 's' : ''} )} diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index ac02870bb1..ea9148d134 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -93,14 +93,14 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } const isNotificationsEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const addedSafes = useAppSelector(selectAllAddedSafes) const totalAddedSafes = useAppSelector(selectTotalAdded) - const { safe } = useSafeInfo() + const { safe, safeAddress } = useSafeInfo() const { query } = useRouter() const onboard = useOnboard() const { dismissPushNotificationBanner, isPushNotificationBannerDismissed } = useDismissPushNotificationsBanner() - const hasAddedSafesOnChain = Object.values(addedSafes[safe.chainId] || {}).length > 0 - const shouldShowBanner = !isPushNotificationBannerDismissed && hasAddedSafesOnChain + const isSafeAdded = !!addedSafes?.[safe.chainId]?.[safeAddress] + const shouldShowBanner = isNotificationsEnabled && !isPushNotificationBannerDismissed && isSafeAdded const { registerNotifications } = useNotificationRegistrations() const { getAllPreferences } = useNotificationPreferences() @@ -139,7 +139,7 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } dismissBanner() } - if (!shouldShowBanner || !isNotificationsEnabled) { + if (!shouldShowBanner) { return children } From 1cb41169e5b79dd1fb13560473a3c5817bc79dd0 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 16:57:14 +0200 Subject: [PATCH 61/62] fix: remove space --- .../settings/PushNotifications/GlobalPushNotifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index 0c13531bb6..7ef79e8f6f 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -316,7 +316,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( We'll ask you to verify ownership of each Safe Account with your signature per chain{' '} - {totalSignaturesRequired} time {totalSignaturesRequired > 1 ? 's' : ''} + {totalSignaturesRequired} time{totalSignaturesRequired > 1 ? 's' : ''} )} From 7b85e74560f8bc339d61f24570d9bc34946d106b Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 25 Sep 2023 18:06:15 +0200 Subject: [PATCH 62/62] fix: e2e --- cypress/e2e/pages/import_export.pages.js | 15 +++++++++++++++ cypress/e2e/smoke/import_export_data.cy.js | 3 +++ 2 files changed, 18 insertions(+) diff --git a/cypress/e2e/pages/import_export.pages.js b/cypress/e2e/pages/import_export.pages.js index 2a0c82fd52..bbd76cdcbe 100644 --- a/cypress/e2e/pages/import_export.pages.js +++ b/cypress/e2e/pages/import_export.pages.js @@ -1,12 +1,14 @@ import { format } from 'date-fns' const path = require('path') +const enablePushNotificationsStr = 'Enable push notifications' const addressBookBtnStr = 'Address book' const dataImportModalStr = 'Data import' const appsBtnStr = 'Apps' const bookmarkedAppsBtnStr = 'Bookmarked apps' const settingsBtnStr = 'Settings' const appearenceTabStr = 'Appearance' +const showMoreTabsBtn = '[data-testid="KeyboardArrowRightIcon"]' const dataTabStr = 'Data' const tab = 'div[role="tablist"] a' export const prependChainPrefixStr = 'Prepend chain prefix to addresses' @@ -41,6 +43,12 @@ export function clickOnImportedSafe(safe) { cy.contains(safe).click() } +export function clickOnClosePushNotificationsBanner() { + cy.waitForSelector(() => { + return cy.get('h6').contains(enablePushNotificationsStr).siblings('.MuiButtonBase-root').click({ force: true }) + }) +} + export function clickOnAddressBookBtn() { cy.contains(addressBookBtnStr).click() } @@ -82,6 +90,13 @@ export function clickOnAppearenceBtn() { cy.contains(tab, appearenceTabStr).click() } +export function clickOnShowMoreTabsBtn() { + cy.get(showMoreTabsBtn).click() +} + +export function verifDataTabBtnIsVisible() { + cy.contains(tab, dataTabStr).should('be.visible') +} export function clickOnDataTab() { cy.contains(tab, dataTabStr).click() } diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index 165ee847e6..55ea6fedae 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -20,6 +20,7 @@ describe('Import Export Data', () => { file.verifyImportModalData() file.clickOnImportBtnDataImportModal() file.clickOnImportedSafe(safe) + file.clickOnClosePushNotificationsBanner() }) it("Verify safe's address book imported data", () => { @@ -45,6 +46,8 @@ describe('Import Export Data', () => { }) it('Verifies data for export in Data tab', () => { + file.clickOnShowMoreTabsBtn() + file.verifDataTabBtnIsVisible() file.clickOnDataTab() file.verifyImportModalData() file.verifyFileDownload()