@@ -69,8 +69,8 @@ export const UnsubscribeModal: React.FC = () => {
- You will stop receiving all notifications from {app.metadata.name} on Web3Inbox
- and connected wallets.
+ You will stop receiving all notifications from {app.metadata.name} on Web3Inbox and
+ connected wallets.
You can re-subscribe at any time later.
diff --git a/src/components/settings/NotificationsSettings/index.tsx b/src/components/settings/NotificationsSettings/index.tsx
index e839684f..20f63f1a 100644
--- a/src/components/settings/NotificationsSettings/index.tsx
+++ b/src/components/settings/NotificationsSettings/index.tsx
@@ -23,7 +23,7 @@ import SettingsToggle from '../SettingsToggle/Index'
import './NotificationsSettings.scss'
const getHelperTooltip = () => {
- switch (window.Notification?.permission) {
+ switch (window.Notification.permission) {
case 'denied':
return 'You have explicitly disabled notifications. Please enable them via your browser or system settings'
case 'granted':
@@ -136,7 +136,7 @@ const NotificationsSettings: React.FC = () => {
- {tokenEntries?.length ? (
+ {tokenEntries.length ? (
{
const handleThemeChange = useCallback(
(modeId: SettingsContextSimpleState['mode']) => {
updateSettings({ mode: modeId })
- // Can't set `mode` directly due it being able to be 'system'
- // setThemeMode(mode === 'dark' ? 'dark' : 'light')
+ /*
+ * Can't set `mode` directly due it being able to be 'system'
+ * setThemeMode(mode === 'dark' ? 'dark' : 'light')
+ */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (localStorage) {
diff --git a/src/components/utils/NotificationPwaModal/index.tsx b/src/components/utils/NotificationPwaModal/index.tsx
index 1d21c8bd..e017065f 100644
--- a/src/components/utils/NotificationPwaModal/index.tsx
+++ b/src/components/utils/NotificationPwaModal/index.tsx
@@ -14,7 +14,7 @@ import './NotificationPwaModal.scss'
export const NotificationPwaModal: React.FC = () => {
const { notifyClientProxy } = useContext(W3iContext)
- const explicitlyDeniedPermissionForNotifications = window.Notification?.permission === 'denied'
+ const explicitlyDeniedPermissionForNotifications = window.Notification.permission === 'denied'
const handleEnableNotifications = async () => {
const notificationPermissionGranted = await requireNotifyPermission()
diff --git a/src/components/utils/PwaModal/index.tsx b/src/components/utils/PwaModal/index.tsx
index bfbb4f42..1760ab66 100644
--- a/src/components/utils/PwaModal/index.tsx
+++ b/src/components/utils/PwaModal/index.tsx
@@ -29,13 +29,17 @@ export const getPlatformInstallText = () => {
const browser = detect()
switch (browser?.name) {
case 'firefox':
- // Firefox on iOS is called Fxios
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox#focus_for_ios
+ /*
+ * Firefox on iOS is called Fxios
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox#focus_for_ios
+ */
case 'fxios':
return 'Install'
case 'chrome':
- // Chrome on iOS is called Crios
- // https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/ios/user_agent.md
+ /*
+ * Chrome on iOS is called Crios
+ * https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/ios/user_agent.md
+ */
case 'crios':
case 'edge-chromium':
return 'Install App'
diff --git a/src/constants/projects.ts b/src/constants/projects.ts
index 2a83cfe3..1f6526a8 100644
--- a/src/constants/projects.ts
+++ b/src/constants/projects.ts
@@ -1,6 +1,6 @@
-import { INotifyApp } from '@/utils/types'
+import type { INotifyApp } from '@/utils/types'
-export const COMING_SOON_PROJECTS: Array
= [
+export const COMING_SOON_PROJECTS: INotifyApp[] = [
{
id: 'galxe',
name: 'Galxe',
diff --git a/src/contexts/SettingsContext/index.tsx b/src/contexts/SettingsContext/index.tsx
index 4298a9f7..a00f1fde 100644
--- a/src/contexts/SettingsContext/index.tsx
+++ b/src/contexts/SettingsContext/index.tsx
@@ -43,7 +43,7 @@ const SettingsContextProvider: React.FC = ({ children
}, [themeColors])
useEffect(() => {
- localStorage?.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settingsState))
+ localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settingsState))
}, [settingsState])
return (
diff --git a/src/contexts/W3iContext/hooks/notifyHooks.ts b/src/contexts/W3iContext/hooks/notifyHooks.ts
index fe258702..aaf70504 100644
--- a/src/contexts/W3iContext/hooks/notifyHooks.ts
+++ b/src/contexts/W3iContext/hooks/notifyHooks.ts
@@ -59,6 +59,7 @@ export const useNotifyState = (w3iProxy: Web3InboxProxy, proxyReady: boolean) =>
const intervalId = setInterval(() => {
if (notifyClient?.hasFinishedInitialLoad()) {
setWatchSubscriptionsComplete(true)
+
return noop
}
refreshNotifyState()
diff --git a/src/hooks/useBreakPoint.ts b/src/hooks/useBreakPoint.ts
index ec1f661d..782c071c 100644
--- a/src/hooks/useBreakPoint.ts
+++ b/src/hooks/useBreakPoint.ts
@@ -8,7 +8,9 @@ export default function useBreakPoint() {
useEffect(() => {
function handleResize() {
- if (typeof window === 'undefined') return
+ if (typeof window === 'undefined') {
+ return
+ }
setWindowWidth(window.innerWidth)
}
diff --git a/src/main.tsx b/src/main.tsx
index 41f55761..8b6e5272 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,8 +3,8 @@ import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createWeb3Modal } from '@web3modal/wagmi/react'
import ReactDOM from 'react-dom/client'
-import { BrowserRouter } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'
+import { BrowserRouter } from 'react-router-dom'
import { WagmiProvider } from 'wagmi'
import { PRIVACY_POLICY_URL, TERMS_OF_SERVICE_URL } from '@/constants/web3Modal'
@@ -54,15 +54,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
-
+
)
diff --git a/src/reducers/notifications.ts b/src/reducers/notifications.ts
index 1350a3d5..5ac09359 100644
--- a/src/reducers/notifications.ts
+++ b/src/reducers/notifications.ts
@@ -1,4 +1,4 @@
-import { NotifyClientTypes } from '@walletconnect/notify-client'
+import type { NotifyClientTypes } from '@walletconnect/notify-client'
export interface TopicNotificationsState {
fullNotifications: NotifyClientTypes.NotifyNotification[]
@@ -7,9 +7,7 @@ export interface TopicNotificationsState {
isLoading: boolean
}
-export interface NotificationsState {
- [topic: string]: TopicNotificationsState
-}
+export type NotificationsState = Record
export type NotificationsActions =
| {
@@ -28,9 +26,11 @@ export type NotificationsActions =
topic: string
}
-// Opted for a reducer since the state changes are complex enough to warrant
-// changes to a set and an array. Having all that inside the hooks would
-// cause too many updates to the hooks, causing unnecessary rerenders.
+/*
+ * Opted for a reducer since the state changes are complex enough to warrant
+ * changes to a set and an array. Having all that inside the hooks would
+ * cause too many updates to the hooks, causing unnecessary rerenders.
+ */
export const notificationsReducer = (
state: NotificationsState,
action: NotificationsActions
@@ -38,12 +38,12 @@ export const notificationsReducer = (
const topicState = state[action.topic] as TopicNotificationsState | undefined
function getTopicState(notifications: NotifyClientTypes.NotifyNotification[]) {
- const ids = topicState?.existingIds || new Set()
+ const ids = topicState?.existingIds ?? new Set()
const filteredNotifications = notifications.filter(val => !ids.has(val.id))
const notificationIds = notifications.map(notification => notification.id)
- const fullNotifications = topicState?.fullNotifications || []
- const newFullIdsSet = new Set(topicState?.existingIds || [])
+ const fullNotifications = topicState?.fullNotifications ?? []
+ const newFullIdsSet = new Set(topicState?.existingIds ?? [])
for (const val of notificationIds) {
newFullIdsSet.add(val)
@@ -67,6 +67,7 @@ export const notificationsReducer = (
}
}
}
+
return state
}
case 'UNSHIFT_NEW_NOTIFICATIONS': {
@@ -81,7 +82,7 @@ export const notificationsReducer = (
...topicState,
existingIds: newFullIdsSet,
fullNotifications: unshiftedNotifications,
- hasMore: topicState?.hasMore || false,
+ hasMore: topicState?.hasMore ?? false,
isLoading: false
}
}
diff --git a/src/utils/address.ts b/src/utils/address.ts
index e2910746..c2aad22d 100644
--- a/src/utils/address.ts
+++ b/src/utils/address.ts
@@ -4,6 +4,7 @@ export const getEthChainAddress = (address?: string) => {
if (!address) {
return ''
}
+
return address.split(':')[2] as `0x${string}`
}
diff --git a/src/utils/assertDefined.ts b/src/utils/assertDefined.ts
new file mode 100644
index 00000000..13e48601
--- /dev/null
+++ b/src/utils/assertDefined.ts
@@ -0,0 +1,7 @@
+export function assertDefined(value: T | null | undefined): T {
+ if (value === null || value === undefined) {
+ throw new Error('Expected value to be defined')
+ }
+
+ return value
+}
diff --git a/src/utils/chain.ts b/src/utils/chain.ts
index 76194e93..71fd3815 100644
--- a/src/utils/chain.ts
+++ b/src/utils/chain.ts
@@ -10,5 +10,5 @@ export const getEIPChainString = (chainId?: number) => {
return undefined
}
- return EIPNumberPrefix.concat(chainId?.toString() || '')
+ return EIPNumberPrefix.concat(chainId.toString() || '')
}
diff --git a/src/utils/ens.ts b/src/utils/ens.ts
index 63d90e02..be300b4b 100644
--- a/src/utils/ens.ts
+++ b/src/utils/ens.ts
@@ -1,4 +1,4 @@
-import { PublicClient } from 'viem'
+import type { PublicClient } from 'viem'
import { normalize } from 'viem/ens'
declare const localStorage: Storage | undefined
diff --git a/src/utils/hooks/notificationHooks.ts b/src/utils/hooks/notificationHooks.ts
index 28eb051e..d1715eff 100644
--- a/src/utils/hooks/notificationHooks.ts
+++ b/src/utils/hooks/notificationHooks.ts
@@ -6,8 +6,10 @@ export const useNotificationPermissionState = () => {
const [notificationPermissionGranted, setNotificationPermissionGranted] =
useState(userEnabledNotification())
- // Can not use navigator.permissions.query({name: 'notifications'}) as it won't work on most
- // mobile browsers
+ /*
+ * Can not use navigator.permissions.query({name: 'notifications'}) as it won't work on most
+ * mobile browsers
+ */
useEffect(() => {
const permissionInterval = setInterval(() => {
diff --git a/src/utils/hooks/useNotificationsInfiniteScroll.tsx b/src/utils/hooks/useNotificationsInfiniteScroll.tsx
index ed77372a..62530251 100644
--- a/src/utils/hooks/useNotificationsInfiniteScroll.tsx
+++ b/src/utils/hooks/useNotificationsInfiniteScroll.tsx
@@ -58,7 +58,7 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
})
}, [notifyClientProxy, dispatch, topic])
- const topicState = topic ? state?.[topic] : undefined
+ const topicState = topic ? state[topic] : undefined
const topicNotifications = topicState ? topicState.fullNotifications : []
const isLoading = topicState ? topicState.isLoading : []
const hasMore = topicState ? topicState.hasMore : false
@@ -95,7 +95,7 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
isLoading,
notifications: topicNotifications,
intersectionObserverRef,
- nextPage: () => nextPageInternal(lastMessageId),
+ nextPage: async () => nextPageInternal(lastMessageId),
unshiftNewMessage
}
}
diff --git a/src/utils/hooks/useNotifyProjects.ts b/src/utils/hooks/useNotifyProjects.ts
index d6e93a66..7d2308da 100644
--- a/src/utils/hooks/useNotifyProjects.ts
+++ b/src/utils/hooks/useNotifyProjects.ts
@@ -43,7 +43,7 @@ const useNotifyProjects = () => {
url: item.dapp_url,
icon: item.image_url?.md ?? '/fallback.svg',
colors: item.metadata?.colors,
- isVerified: Boolean(item.is_verified || item.isVerified),
+ isVerified: Boolean(item.is_verified ?? item.isVerified),
isFeatured: item.is_featured,
isComingSoon: item.is_coming_soon
}))
diff --git a/src/utils/idb.ts b/src/utils/idb.ts
index ef079b00..a5aeae70 100644
--- a/src/utils/idb.ts
+++ b/src/utils/idb.ts
@@ -16,7 +16,8 @@ export const ECHO_REGISTRATION_STORE = 'echo-registration-store'
const STORE_NAMES = [SYMKEY_OBJ_STORE, ECHO_REGISTRATION_STORE]
-/* DATABASE_VERSION should be incremented if and when any schema changes occur
+/*
+ * DATABASE_VERSION should be incremented if and when any schema changes occur
* This involves changing store names above, adding stores, or changing the schema
* in any other way.
*/
@@ -45,15 +46,15 @@ export const getIndexedDbStore = async (
}
})
- const getItem = (key: string): Promise => {
+ const getItem = async (key: string): Promise => {
return db.get(storeName, key)
}
- const setItem = (key: string, value: string): Promise => {
+ const setItem = async (key: string, value: string): Promise => {
return db.put(storeName, value, key)
}
- const getAllKeys = (): Promise => {
+ const getAllKeys = async (): Promise => {
return db.getAllKeys(storeName)
}
diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts
index 0ec945ce..fef76c33 100644
--- a/src/utils/localStorage.ts
+++ b/src/utils/localStorage.ts
@@ -11,6 +11,6 @@ export class LocalStorage {
return undefined
}
- return localStorage.setItem(key, value)
+ localStorage.setItem(key, value)
}
}
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index 34b0c38b..5f31d117 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -1,4 +1,4 @@
-import { NotifyClient } from '@walletconnect/notify-client'
+import type { NotifyClient } from '@walletconnect/notify-client'
import { localStorageKeys } from '@/constants/localStorage'
import { LocalStorage } from '@/utils/localStorage'
@@ -15,13 +15,17 @@ const callEcho = async (clientId: string, token: string) => {
// Check for existing registration to prevent spamming echo
const existingRegistrationToken = await getRegistrationToken(clientId)
- // Already registered device.
- // No need to spam echo
+ /*
+ * Already registered device.
+ * No need to spam echo
+ */
if (existingRegistrationToken === token) {
- // Do not check for existing registration token.
- // Echo is meant to be called repeatedly to refresh PN token
- // Console log for purposes of debugging if an error relating to echo
- // happens
+ /*
+ * Do not check for existing registration token.
+ * Echo is meant to be called repeatedly to refresh PN token
+ * Console log for purposes of debugging if an error relating to echo
+ * happens
+ */
console.log(
'main-sw > registerWithEcho > user already registered with token',
token,
@@ -70,6 +74,7 @@ export const closeNotificationModal = () => {
export const checkIfNotificationModalClosed = () => {
const storageValue = LocalStorage.get(localStorageKeys.notificationModalClosed)
+
return storageValue === 'true'
}
@@ -79,8 +84,9 @@ export const notificationsAvailableInBrowser = () => {
export const userEnabledNotification = () => {
if (notificationsAvailableInBrowser()) {
- return window.Notification?.permission === 'granted'
+ return window.Notification.permission === 'granted'
}
+
return false
}
@@ -91,19 +97,23 @@ export const userEnabledNotification = () => {
export const requireNotifyPermission = async () => {
if (!notificationsAvailableInBrowser()) {
console.warn('This browser does not support desktop push notifications')
+
return false
}
- // No need to explicitly check for Notifications here since
- // the above check ensures it exists
- switch (window.Notification?.permission) {
+ /*
+ * No need to explicitly check for Notifications here since
+ * the above check ensures it exists
+ */
+ switch (window.Notification.permission) {
case 'granted':
return true
case 'denied':
console.warn('User denied permissions')
+
return false
default:
- return (await window.Notification?.requestPermission()) === 'granted'
+ return (await window.Notification.requestPermission()) === 'granted'
}
}
diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts
index ca88abf7..220ae68f 100644
--- a/src/utils/sentry.ts
+++ b/src/utils/sentry.ts
@@ -7,7 +7,7 @@ export const initSentry = () => {
new Sentry.BrowserTracing({
tracePropagationTargets: ['https://web3inbox-dev-hidden.vercel.app']
}),
- new Sentry.Replay(),
+ new Sentry.Replay()
],
tracesSampleRate: 0.2,
replaysSessionSampleRate: 0.1,
diff --git a/src/utils/signature.ts b/src/utils/signature.ts
index 0c86c366..f88623d1 100644
--- a/src/utils/signature.ts
+++ b/src/utils/signature.ts
@@ -1,13 +1,14 @@
import { getBytecode } from '@wagmi/core'
-import { wagmiConfig } from './wagmiConfig';
+import { wagmiConfig } from './wagmiConfig'
export const isSmartContractWallet = async (address: `0x${string}`) => {
const bytecode = await getBytecode(wagmiConfig, {
address
})
- const nonContractBytecode = !bytecode || bytecode === '0x' || bytecode === '0x0' || bytecode === '0x00';
+ const nonContractBytecode =
+ !bytecode || bytecode === '0x' || bytecode === '0x0' || bytecode === '0x00'
- return !nonContractBytecode;
+ return !nonContractBytecode
}
diff --git a/src/utils/ui.ts b/src/utils/ui.ts
index 9495e53f..7f4aa03b 100644
--- a/src/utils/ui.ts
+++ b/src/utils/ui.ts
@@ -49,7 +49,10 @@ export const screenBreakPoints = {
}
export const isMobile = () => {
- if (typeof window === 'undefined') return false
+ if (typeof window === 'undefined') {
+ return false
+ }
+
return window.innerWidth < screenBreakPoints.md
}
diff --git a/src/w3iProxy/authProviders/internalAuthProvider.ts b/src/w3iProxy/authProviders/internalAuthProvider.ts
index 6969f46d..971e4eee 100644
--- a/src/w3iProxy/authProviders/internalAuthProvider.ts
+++ b/src/w3iProxy/authProviders/internalAuthProvider.ts
@@ -22,6 +22,7 @@ export default class InternalAuthProvider {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!data.address) {
this.emitter.emit('auth_set_account', { account: null, chain: null })
+
return
}
diff --git a/src/w3iProxy/index.ts b/src/w3iProxy/index.ts
index 3bd746ec..a8c45c5b 100644
--- a/src/w3iProxy/index.ts
+++ b/src/w3iProxy/index.ts
@@ -9,11 +9,11 @@ import type { Logger } from 'pino'
import type { UiEnabled } from '@/contexts/W3iContext/context'
import { identifyMixpanelUserAndInit } from '@/utils/mixpanel'
+import { showErrorMessageToast } from '@/utils/toasts'
import { wagmiConfig } from '@/utils/wagmiConfig'
import W3iAuthFacade from '@/w3iProxy/w3iAuthFacade'
import type W3iChatFacade from '@/w3iProxy/w3iChatFacade'
import W3iNotifyFacade from '@/w3iProxy/w3iNotifyFacade'
-import { showErrorMessageToast } from '@/utils/toasts'
export type W3iChatClient = Omit
export type W3iNotifyClient = Omit
@@ -110,8 +110,8 @@ class Web3InboxProxy {
return signed
} catch (e: any) {
- showErrorMessageToast("Failed to sign message. Consider using different wallet.")
- throw new Error(`Failed to sign message. ${e.message}`)
+ showErrorMessageToast('Failed to sign message. Consider using different wallet.')
+ throw new Error(`Failed to sign message. ${e.message}`)
}
}
}
diff --git a/src/w3iProxy/listenerTypes.ts b/src/w3iProxy/listenerTypes.ts
index 886b2198..65aebe1b 100644
--- a/src/w3iProxy/listenerTypes.ts
+++ b/src/w3iProxy/listenerTypes.ts
@@ -21,12 +21,12 @@ export interface ChatFacadeEvents {
sync_update: never
}
-type NextAction = {
+interface NextAction {
type: T
params: Parameters[0]
}
-type NextActions = NextAction<'subscribe'> | NextAction<'update'> | NextAction<'deleteSubscription'>
+type NextActions = NextAction<'deleteSubscription'> | NextAction<'subscribe'> | NextAction<'update'>
export interface NotifyFacadeEvents {
notify_message: NotifyClientTypes.EventArguments['notify_message']
diff --git a/src/w3iProxy/notifyProviders/externalNotifyProvider.ts b/src/w3iProxy/notifyProviders/externalNotifyProvider.ts
index e44ae900..75329db8 100644
--- a/src/w3iProxy/notifyProviders/externalNotifyProvider.ts
+++ b/src/w3iProxy/notifyProviders/externalNotifyProvider.ts
@@ -80,12 +80,10 @@ export default class ExternalNotifyProvider implements W3iNotifyProvider {
public async unregisterOtherAccounts() {
// TODO: remove this whole provider
- return
}
public async unregister() {
// TODO: remove this whole provider
- return
}
public async register() {
diff --git a/src/w3iProxy/notifyProviders/internalNotifyProvider.ts b/src/w3iProxy/notifyProviders/internalNotifyProvider.ts
index a82f6ac2..22f5df48 100644
--- a/src/w3iProxy/notifyProviders/internalNotifyProvider.ts
+++ b/src/w3iProxy/notifyProviders/internalNotifyProvider.ts
@@ -13,8 +13,8 @@ import {
setupSubscriptionsSymkeys,
userEnabledNotification
} from '@/utils/notifications'
-import { W3iNotifyProvider } from '@/w3iProxy/notifyProviders/types'
import { isSmartContractWallet } from '@/utils/signature'
+import type { W3iNotifyProvider } from '@/w3iProxy/notifyProviders/types'
export default class InternalNotifyProvider implements W3iNotifyProvider {
private notifyClient: NotifyClient | undefined
@@ -70,7 +70,7 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
* the service worker echo registration logic is also idempotent
*/
private async ensureEchoRegistration() {
- // impossible case, just here to please typescript
+ // Impossible case, just here to please typescript
if (!this.notifyClient) {
return
}
@@ -112,13 +112,14 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
// ------------------- Method-forwarding for NotifyClient -------------------
public hasFinishedInitialLoad() {
- return this.notifyClient?.hasFinishedInitialLoad() || false
+ return this.notifyClient?.hasFinishedInitialLoad() ?? false
}
public async unregister(params: { account: string }) {
if (!this.notifyClient) {
throw new Error(this.formatClientRelatedError('unregister'))
}
+
return this.notifyClient.unregister(params)
}
@@ -130,22 +131,22 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
const props = {
account: params.account,
domain: params.domain,
- allApps: !Boolean(params.isLimited)
+ allApps: !params.isLimited
}
if (this.notifyClient.isRegistered(props)) {
return this.notifyClient.identityKeys.getIdentity({ account: props.account })
- } else {
- // this means that there is a stale registration
- if (await this.notifyClient.identityKeys.hasIdentity({ account: props.account })) {
- await this.notifyClient.unregister({ account: props.account })
- }
+ }
+ // This means that there is a stale registration
+ if (await this.notifyClient.identityKeys.hasIdentity({ account: props.account })) {
+ await this.notifyClient.unregister({ account: props.account })
}
const preparedRegistration = await this.notifyClient.prepareRegistration(props)
const signature = await (async message => {
this.emitter.emit('notify_signature_requested', { message })
+
return new Promise(resolve => {
this.emitter.on(
'notify_signature_delivered',
@@ -157,14 +158,14 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
})
})(preparedRegistration.message)
- const [,,address] = props.account.split(':')
+ const [, , address] = props.account.split(':')
- const isEip1271Signature = await isSmartContractWallet(address as `0x${string}`);
+ const isEip1271Signature = await isSmartContractWallet(address as `0x${string}`)
const identityKey = await this.notifyClient.register({
registerParams: preparedRegistration.registerParams,
signature,
- signatureType: isEip1271Signature? 'eip1271' : 'eip191'
+ signatureType: isEip1271Signature ? 'eip1271' : 'eip191'
})
return identityKey
@@ -189,10 +190,11 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
params
}
})
+
return false
}
- let subscribed: boolean = false
+ let subscribed = false
try {
subscribed = await this.notifyClient.subscribe({
...params
@@ -234,6 +236,7 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
params
}
})
+
return false
}
@@ -261,6 +264,7 @@ export default class InternalNotifyProvider implements W3iNotifyProvider {
params
}
})
+
return
}
diff --git a/tests/home.spec.ts b/tests/home.spec.ts
index 417eef13..72f8a7f7 100644
--- a/tests/home.spec.ts
+++ b/tests/home.spec.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'
test('has title', async ({ page }) => {
await page.goto('/')
- await expect(page).toHaveTitle(/Web3Inbox/)
+ await expect(page).toHaveTitle(/Web3Inbox/u)
})
test('welcome message', async ({ page }) => {
diff --git a/tests/shared/constants/index.ts b/tests/shared/constants/index.ts
index 97179939..2305f147 100644
--- a/tests/shared/constants/index.ts
+++ b/tests/shared/constants/index.ts
@@ -1,20 +1,29 @@
import type { SessionParams } from '../types'
// Allow localhost
-export const BASE_URL = process.env['BASE_URL'] || 'http://localhost:5173/'
-export const WALLET_URL = process.env['WALLET_URL'] || 'https://react-wallet.walletconnect.com/'
+export const BASE_URL = process.env.BASE_URL ?? 'http://localhost:5173/'
+export const WALLET_URL = process.env.WALLET_URL ?? 'https://react-wallet.walletconnect.com/'
export const DEFAULT_SESSION_PARAMS: SessionParams = {
reqAccounts: ['1', '2'],
optAccounts: ['1', '2'],
accept: true
}
+function getRequiredEnvVar(name: string): string {
+ const value = process.env[name]
+ if (!value) {
+ throw new Error(`Missing required environment variable: ${name}`)
+ }
+
+ return value
+}
+
export const CUSTOM_TEST_DAPP = {
- description: "Test description",
- icons: ["https://i.imgur.com/q9QDRXc.png"],
- name: "Notify Swift Integration Tests Prod",
- appDomain: "wc-notify-swift-integration-tests-prod.pages.dev",
- projectSecret: process.env['TEST_DAPP_PROJECT_SECRET'],
- projectId: process.env['TEST_DAPP_PROJECT_ID'],
- messageType: "f173f231-a45c-4dc0-aa5d-956eb04f7360"
-} as const;
+ description: 'Test description',
+ icons: ['https://i.imgur.com/q9QDRXc.png'],
+ name: 'Notify Swift Integration Tests Prod',
+ appDomain: 'wc-notify-swift-integration-tests-prod.pages.dev',
+ projectSecret: getRequiredEnvVar('TEST_DAPP_PROJECT_SECRET'),
+ projectId: getRequiredEnvVar('TEST_DAPP_PROJECT_ID'),
+ notificationType: 'f173f231-a45c-4dc0-aa5d-956eb04f7360'
+} as const
diff --git a/tests/shared/fixtures/fixture.ts b/tests/shared/fixtures/fixture.ts
index 837cc8b1..541d06af 100644
--- a/tests/shared/fixtures/fixture.ts
+++ b/tests/shared/fixtures/fixture.ts
@@ -1,9 +1,9 @@
import { test as base } from '@playwright/test'
+import { NotifyServer } from '../helpers/notifyServer'
import { InboxPage } from '../pages/InboxPage'
-import { InboxValidator } from '../validators/ModalValidator'
import { SettingsPage } from '../pages/SettingsPage'
-import { NotifyServer } from '../helpers/notifyServer'
+import { InboxValidator } from '../validators/ModalValidator'
// Declare the types of fixtures to use
export interface ModalFixture {
@@ -25,14 +25,14 @@ export const test = base.extend({
await use(modalValidator)
},
// Have to pass same page object to maintain state between pages
- settingsPage: async({ inboxPage }, use) => {
+ settingsPage: async ({ inboxPage }, use) => {
const settingsPage = new SettingsPage(inboxPage.page)
- settingsPage.load()
- use(settingsPage)
- },
- notifyServer: async({}, use) => {
- const notifyServer = new NotifyServer();
- use(notifyServer)
+ await use(settingsPage)
},
+ // eslint-disable-next-line no-empty-pattern
+ notifyServer: async ({}, use) => {
+ const notifyServer = new NotifyServer()
+ await use(notifyServer)
+ }
})
export { expect } from '@playwright/test'
diff --git a/tests/shared/helpers/notifyServer.ts b/tests/shared/helpers/notifyServer.ts
index 620636ce..6d424606 100644
--- a/tests/shared/helpers/notifyServer.ts
+++ b/tests/shared/helpers/notifyServer.ts
@@ -1,7 +1,7 @@
-import { expect } from "@playwright/test"
+import { expect } from '@playwright/test'
export class NotifyServer {
- private notifyBaseUrl = "https://notify.walletconnect.com"
+ private readonly notifyBaseUrl = 'https://notify.walletconnect.com'
public async sendMessage({
projectId,
@@ -13,23 +13,23 @@ export class NotifyServer {
icon,
type
}: {
- projectId: string,
- projectSecret: string,
+ projectId: string
+ projectSecret: string
accounts: string[]
- title: string,
- body: string,
- icon: string,
+ title: string
+ body: string
+ icon: string
url: string
type: string
}) {
const request = JSON.stringify({
accounts,
notification: {
- title,
- body,
- icon,
- url,
- type
+ title,
+ body,
+ icon,
+ url,
+ type
}
})
@@ -37,16 +37,19 @@ export class NotifyServer {
const headers = new Headers({
Authorization: `Bearer ${projectSecret}`,
- "Content-Type": "application/json"
+ 'Content-Type': 'application/json'
})
const fetchResults = await fetch(fetchUrl, {
- method: "POST",
+ method: 'POST',
headers,
body: request
})
- console.log({fetchResultsStatus: fetchResults.status, fetchResults: await fetchResults.text()})
+ console.log({
+ fetchResultsStatus: fetchResults.status,
+ fetchResults: await fetchResults.text()
+ })
expect(fetchResults.status).toEqual(200)
}
diff --git a/tests/shared/pages/DeviceRegistrationPage.ts b/tests/shared/pages/DeviceRegistrationPage.ts
index 6afd0b48..7f4ce993 100644
--- a/tests/shared/pages/DeviceRegistrationPage.ts
+++ b/tests/shared/pages/DeviceRegistrationPage.ts
@@ -1,16 +1,16 @@
import type { Page } from '@playwright/test'
export class DeviceRegistrationPage {
- constructor(
+ public constructor(
public readonly page: Page,
public readonly url: string
) {}
- async load() {
+ public async load() {
await this.page.goto(this.url)
}
- async approveDevice() {
+ public async approveDevice() {
await this.page.getByRole('button', { name: 'Approve' }).click()
}
}
diff --git a/tests/shared/pages/InboxPage.ts b/tests/shared/pages/InboxPage.ts
index 55a76dd5..4bd4c893 100644
--- a/tests/shared/pages/InboxPage.ts
+++ b/tests/shared/pages/InboxPage.ts
@@ -1,5 +1,6 @@
import { type Locator, type Page, expect } from '@playwright/test'
+import { assertDefined } from '../../../src/utils/assertDefined'
import { BASE_URL } from '../constants'
export class InboxPage {
@@ -7,22 +8,22 @@ export class InboxPage {
private readonly connectButton: Locator
- constructor(public readonly page: Page) {
+ public constructor(public readonly page: Page) {
this.connectButton = this.page.getByRole('button', { name: 'Connect Wallet' })
}
- async load() {
+ public async load() {
await this.page.goto(this.baseURL)
}
- async gotoDiscoverPage() {
+ public async gotoDiscoverPage() {
await this.page.locator('.Sidebar__Navigation__Link[href="/notifications"]').click()
- await this.page.getByText('Discover Apps').click();
+ await this.page.getByText('Discover Apps').click()
- await this.page.getByText('Discover Web3Inbox').isVisible();
+ await this.page.getByText('Discover Web3Inbox').isVisible()
}
- async copyConnectUriToClipboard() {
+ public async copyConnectUriToClipboard() {
await this.page.goto(this.baseURL)
await this.connectButton.click()
await this.page.getByTestId('wallet-selector-walletconnect').click()
@@ -30,49 +31,54 @@ export class InboxPage {
await this.page.getByTestId('copy-wc2-uri').click()
}
- async disconnect() {
+ public async disconnect() {
await this.page.getByTestId('account-button').click()
await this.page.getByTestId('disconnect-button').click()
}
- async promptSiwe() {
+ public async promptSiwe() {
await this.page.getByRole('button', { name: 'Sign in with wallet' }).click()
}
- async rejectNotifications() {
+ public async rejectNotifications() {
// Allow for the modal to pop up
await this.page.waitForTimeout(4000)
const isVisible = (await this.page.locator('.NotificationPwaModal__close-button').count()) > 0
- if (!isVisible) return
+ if (!isVisible) {
+ return
+ }
await this.page.locator('.NotificationPwaModal__close-button').first().click()
}
- async getAddress() {
+ public async getAddress() {
await this.page.locator('.Avatar').first().click()
const address = await this.page.locator('wui-avatar').getAttribute('alt')
- await this.page.locator('wui-icon[name=close]').first().click();
+ await this.page.locator('wui-icon[name=close]').first().click()
- return address;
+ return assertDefined(address)
}
- async subscribe(nth: number) {
+ public async subscribe(nth: number) {
const appCard = this.page.locator('.AppCard__body').nth(nth)
await appCard.locator('.AppCard__body__subscribe').click()
- await appCard.locator('.AppCard__body__subscribed').getByText('Subscribed', { exact: false }).isVisible()
+ await appCard
+ .locator('.AppCard__body__subscribed')
+ .getByText('Subscribed', { exact: false })
+ .isVisible()
}
- async navigateToNewSubscription(nth: number) {
+ public async navigateToNewSubscription(nth: number) {
await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).click()
await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).isHidden()
}
- async subscribeAndNavigateToDapp(nth: number) {
- await this.subscribe(nth);
- await this.navigateToNewSubscription(nth);
+ public async subscribeAndNavigateToDapp(nth: number) {
+ await this.subscribe(nth)
+ await this.navigateToNewSubscription(nth)
}
- async unsubscribe() {
+ public async unsubscribe() {
await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click()
await this.page.getByRole('button', { name: 'Unsubscribe' }).click()
await this.page.getByRole('button', { name: 'Unsubscribe' }).nth(1).click()
@@ -80,32 +86,39 @@ export class InboxPage {
await this.page.waitForTimeout(2000)
}
- async navigateToDappFromSidebar(nth: number) {
+ public async navigateToDappFromSidebar(nth: number) {
await this.page.locator('.AppSelector__notifications-link').nth(nth).click()
}
- async countSubscribedDapps() {
+ public async countSubscribedDapps() {
const notificationsCount = await this.page.locator('.AppSelector__notifications').count()
-
- return notificationsCount - 1;
+
+ return notificationsCount - 1
}
/**
* Waits for a specific number of dApps to be subscribed.
- *
+ *
* @param {number} expectedCount - The expected number of dApps to wait for.
* @returns {Promise}
*/
- async waitForSubscriptions(expectedCount: number): Promise {
- // Wait for a function that checks the length of a list or a set of elements
- // matching a certain condition to equal the expectedCount.
- await this.page.waitForFunction(([className, count]) => {
- const elements = document.getElementsByClassName(className)[1].children;;
- return elements.length === count;
- }, ['AppSelector__list', expectedCount] as const, { timeout: 5000 });
- }
-
- async updatePreferences() {
+ public async waitForSubscriptions(expectedCount: number): Promise {
+ /*
+ * Wait for a function that checks the length of a list or a set of elements
+ * matching a certain condition to equal the expectedCount.
+ */
+ await this.page.waitForFunction(
+ ([className, count]) => {
+ const elements = document.getElementsByClassName(className)[1].children
+
+ return elements.length === count
+ },
+ ['AppSelector__list', expectedCount] as const,
+ { timeout: 5000 }
+ )
+ }
+
+ public async updatePreferences() {
await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click()
await this.page.getByRole('button', { name: 'Preferences' }).click()
// Ensure the modal is visible
@@ -119,22 +132,23 @@ export class InboxPage {
await this.page.getByRole('button', { name: 'Update' }).click()
-
await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click()
await this.page.getByRole('button', { name: 'Preferences' }).click()
- const firstCheckBoxIsCheckedAfterUpdating = await this.page.isChecked('.Toggle__checkbox:nth-of-type(1)')
+ const firstCheckBoxIsCheckedAfterUpdating = await this.page.isChecked(
+ '.Toggle__checkbox:nth-of-type(1)'
+ )
expect(firstCheckBoxIsChecked).not.toEqual(firstCheckBoxIsCheckedAfterUpdating)
- await this.page.locator('.PreferencesModal__close').click();
+ await this.page.locator('.PreferencesModal__close').click()
}
- async cancelSiwe() {
+ public async cancelSiwe() {
await this.page.getByTestId('w3m-connecting-siwe-cancel').click()
}
- async switchNetwork(network: string) {
+ public async switchNetwork(network: string) {
await this.page.getByTestId('account-button').click()
await this.page.getByTestId('w3m-account-select-network').click()
await this.page.getByTestId(`w3m-network-switch-${network}`).click()
diff --git a/tests/shared/pages/SettingsPage.ts b/tests/shared/pages/SettingsPage.ts
index cbb46e70..35360269 100644
--- a/tests/shared/pages/SettingsPage.ts
+++ b/tests/shared/pages/SettingsPage.ts
@@ -1,21 +1,18 @@
-import { type Locator, type Page, expect } from '@playwright/test'
+import type { Page } from '@playwright/test'
import { BASE_URL } from '../../shared/constants'
export class SettingsPage {
private readonly baseURL = BASE_URL
- constructor(public readonly page: Page) {}
+ public constructor(public readonly page: Page) {}
- async load() {}
-
- async goToNotificationSettings() {
+ public async goToNotificationSettings() {
await this.page.locator('.Sidebar__Navigation__Link[href="/settings"]').click()
}
- async displayCustomDapp(dappUrl: string) {
+ public async displayCustomDapp(dappUrl: string) {
await this.page.getByPlaceholder('app.example.com').fill(dappUrl)
- await this.page.getByRole('button', { name: "Save", exact: true}).click()
+ await this.page.getByRole('button', { name: 'Save', exact: true }).click()
}
-
}
diff --git a/tests/shared/pages/WalletPage.ts b/tests/shared/pages/WalletPage.ts
index ba1c0360..7218afcf 100644
--- a/tests/shared/pages/WalletPage.ts
+++ b/tests/shared/pages/WalletPage.ts
@@ -10,16 +10,16 @@ export class WalletPage {
private readonly gotoHome: Locator
private readonly vercelPreview: Locator
- constructor(public readonly page: Page) {
+ public constructor(public readonly page: Page) {
this.gotoHome = this.page.getByTestId('wc-connect')
this.vercelPreview = this.page.locator('css=vercel-live-feedback')
}
- async load() {
+ public async load() {
await this.page.goto(this.baseURL)
}
- async connect() {
+ public async connect() {
const isVercelPreview = (await this.vercelPreview.count()) > 0
if (isVercelPreview) {
await this.vercelPreview.evaluate((iframe: HTMLIFrameElement) => iframe.remove())
@@ -40,7 +40,7 @@ export class WalletPage {
* @param optAccounts - optional account numbers to select ex/ ['1', '2']
* @param accept - accept or reject the session
*/
- async handleSessionProposal(opts: SessionParams) {
+ public async handleSessionProposal(opts: SessionParams) {
const variant = opts.accept ? `approve` : `reject`
// `.click` doesn't work here, so we use `.focus` and `Space`
await this.page.getByTestId(`session-${variant}-button`).isEnabled()
@@ -48,7 +48,7 @@ export class WalletPage {
await this.page.keyboard.press('Space')
}
- async handleRequest({ accept }: { accept: boolean }) {
+ public async handleRequest({ accept }: { accept: boolean }) {
const variant = accept ? `approve` : `reject`
// `.click` doesn't work here, so we use `.focus` and `Space`
await this.page.getByTestId(`session-${variant}-button`).isEnabled()
diff --git a/tests/shared/validators/ModalValidator.ts b/tests/shared/validators/ModalValidator.ts
index b043219d..300958d7 100644
--- a/tests/shared/validators/ModalValidator.ts
+++ b/tests/shared/validators/ModalValidator.ts
@@ -2,36 +2,36 @@ import { expect } from '@playwright/test'
import type { Page } from '@playwright/test'
export class InboxValidator {
- constructor(public readonly page: Page) {}
+ public constructor(public readonly page: Page) {}
- async expectConnected() {
+ public async expectConnected() {
await expect(this.page.getByTestId('account-button')).toBeVisible()
}
- async expectAuthenticated() {
+ public async expectAuthenticated() {
await expect(this.page.getByTestId('w3m-authentication-status')).toContainText('authenticated')
}
- async expectUnauthenticated() {
+ public async expectUnauthenticated() {
await expect(this.page.getByTestId('w3m-authentication-status')).toContainText(
'unauthenticated'
)
}
- async expectSignatureDeclined() {
+ public async expectSignatureDeclined() {
await expect(this.page.getByText('Signature declined')).toBeVisible()
}
- async expectDisconnected() {
+ public async expectDisconnected() {
await expect(this.page.getByTestId('account-button')).not.toBeVisible()
}
- async expectAcceptedSign() {
+ public async expectAcceptedSign() {
// We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element.
await expect(this.page.getByText('abc')).toBeVisible()
}
- async expectRejectedSign() {
+ public async expectRejectedSign() {
// We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element.
await expect(this.page.getByText('abc')).toBeVisible()
}
diff --git a/tests/shared/validators/WalletValidator.ts b/tests/shared/validators/WalletValidator.ts
index 2f7a0305..392ae4bf 100644
--- a/tests/shared/validators/WalletValidator.ts
+++ b/tests/shared/validators/WalletValidator.ts
@@ -4,21 +4,21 @@ import type { Locator, Page } from '@playwright/test'
export class WalletValidator {
private readonly gotoSessions: Locator
- constructor(public readonly page: Page) {
+ public constructor(public readonly page: Page) {
this.gotoSessions = this.page.getByTestId('sessions')
}
- async expectConnected() {
+ public async expectConnected() {
await this.gotoSessions.click()
await expect(this.page.getByTestId('session-card')).toBeVisible()
}
- async expectDisconnected() {
+ public async expectDisconnected() {
await this.gotoSessions.click()
await expect(this.page.getByTestId('session-card')).not.toBeVisible()
}
- async expectReceivedSign({ chainName = 'Ethereum' }) {
+ public async expectReceivedSign({ chainName = 'Ethereum' }) {
await expect(this.page.getByTestId('session-approve-button')).toBeVisible()
await expect(this.page.getByTestId('request-details-chain')).toHaveText(chainName)
}
diff --git a/tests/subscribe.spec.ts b/tests/subscribe.spec.ts
index cb54d4cc..ca9d5bc2 100644
--- a/tests/subscribe.spec.ts
+++ b/tests/subscribe.spec.ts
@@ -75,23 +75,25 @@ test('it should subscribe and unsubscribe to and from multiple dapps', async ({
// Wait for the 2 dapps to be subscribed to.
await inboxPage.page.waitForFunction(() => {
// Using 1 here since the first `AppSelector__list` is the one with `Discover Apps`
- const apps = document.getElementsByClassName('AppSelector__list')[1].children.length;
- return apps === 2;
+ const apps = document.getElementsByClassName('AppSelector__list')[1].children.length
+
+ return apps === 2
})
- expect(await inboxPage.countSubscribedDapps()).toEqual(2);
+ expect(await inboxPage.countSubscribedDapps()).toEqual(2)
- await inboxPage.navigateToDappFromSidebar(0);
+ await inboxPage.navigateToDappFromSidebar(0)
await inboxPage.unsubscribe()
- expect(await inboxPage.countSubscribedDapps()).toEqual(1);
+ expect(await inboxPage.countSubscribedDapps()).toEqual(1)
- // select 0 again since we unsubscribed from the second dapp
- // so there is only one item
- await inboxPage.navigateToDappFromSidebar(0);
+ /*
+ * Select 0 again since we unsubscribed from the second dapp
+ * so there is only one item
+ */
+ await inboxPage.navigateToDappFromSidebar(0)
await inboxPage.unsubscribe()
})
-
test('it should subscribe, receive messages and unsubscribe', async ({
inboxPage,
walletPage,
@@ -115,30 +117,30 @@ test('it should subscribe, receive messages and unsubscribe', async ({
await inboxPage.gotoDiscoverPage()
// Ensure the custom dapp is the one subscribed to
- await inboxPage.page.getByText("Notify Swift", {exact: false}).waitFor({ state: 'visible' })
+ await inboxPage.page.getByText('Notify Swift', { exact: false }).waitFor({ state: 'visible' })
+
+ expect(await inboxPage.page.getByText('Notify Swift', { exact: false }).isVisible()).toEqual(true)
- expect(await inboxPage.page.getByText("Notify Swift", {exact: false}).isVisible()).toEqual(true);
-
await inboxPage.subscribeAndNavigateToDapp(0)
- if(!CUSTOM_TEST_DAPP.projectId || !(CUSTOM_TEST_DAPP.projectSecret)) {
- throw new Error("TEST_DAPP_SECRET and TEST_DAPP_ID are required")
+ if (!CUSTOM_TEST_DAPP.projectId || !CUSTOM_TEST_DAPP.projectSecret) {
+ throw new Error('TEST_DAPP_SECRET and TEST_DAPP_ID are required')
}
const address = await inboxPage.getAddress()
await notifyServer.sendMessage({
accounts: [`eip155:1:${address}`],
- body: "Test Body",
- title: "Test Title",
- type: CUSTOM_TEST_DAPP.messageType,
+ body: 'Test Body',
+ title: 'Test Title',
+ type: CUSTOM_TEST_DAPP.notificationType,
url: CUSTOM_TEST_DAPP.appDomain,
icon: CUSTOM_TEST_DAPP.icons[0],
projectId: CUSTOM_TEST_DAPP.projectId,
- projectSecret: CUSTOM_TEST_DAPP.projectSecret,
+ projectSecret: CUSTOM_TEST_DAPP.projectSecret
})
- await inboxPage.page.getByText("Test Body").waitFor({state: 'visible'})
+ await inboxPage.page.getByText('Test Body').waitFor({ state: 'visible' })
- expect(await inboxPage.page.getByText("Test Body").isVisible()).toEqual(true)
+ expect(await inboxPage.page.getByText('Test Body').isVisible()).toEqual(true)
})
diff --git a/yarn.lock b/yarn.lock
index 137534ba..f5a6e18f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4795,6 +4795,11 @@ domain-browser@^4.22.0:
resolved "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz"
integrity sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==
+dotenv@16.3.1:
+ version "16.3.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
+ integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
+
duplexify@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz"