Skip to content

Commit

Permalink
fix: prevent unnecessary network toasts (#1448)
Browse files Browse the repository at this point in the history
Signed-off-by: Bryce McMath <[email protected]>
  • Loading branch information
bryce-mcmath authored Feb 20, 2025
1 parent 8dac71d commit d391221
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 121 deletions.
74 changes: 18 additions & 56 deletions packages/legacy/core/App/components/network/NetInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,36 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useEffect, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from 'react-native-toast-message'
import { useNetwork } from '../../contexts/network'
import { ToastType } from '../../components/toast/BaseToast'
import { TOKENS, useServices } from '../../container-api'

const NetInfo: React.FC = () => {
const { silentAssertConnectedNetwork, assertInternetReachable, assertMediatorReachable } = useNetwork()
const [{ disableMediatorCheck }] = useServices([TOKENS.CONFIG])
const { assertInternetReachable } = useNetwork()
const { t } = useTranslation()
const [hasShown, setHasShown] = useState(false)

const isConnected = silentAssertConnectedNetwork()
const showNetworkWarning = useCallback(() => {
setHasShown(true)
Toast.show({
type: ToastType.Error,
autoHide: true,
text1: t('NetInfo.NoInternetConnectionTitle'),
})
}, [t])

useEffect(() => {
const _showNetworkWarning = () => {
setHasShown(true)
Toast.show({
type: ToastType.Error,
autoHide: true,
text1: t('NetInfo.NoInternetConnectionTitle'),
})
const internetReachable = assertInternetReachable()
if (internetReachable) {
Toast.hide()
}
// Network is available, do further testing according to CFG.disableMediatorCheck
if (!disableMediatorCheck) {
// Network is available
if (isConnected) {
// Check mediator socket, also assert internet reachable
assertMediatorReachable().then((status) => {
if (status) {
Toast.hide()
return
} else {
// Network is available but cannot access nediator, display toast
_showNetworkWarning()
}
})
return
} else if (!hasShown) {
_showNetworkWarning()
}
return
} else {
// Check internetReachable by connecting test beacon urls
assertInternetReachable().then((status) => {
if (status) {
Toast.hide()
return
} else if (null === status) {
// keep silent when the internet status not yet assert
return
/*
Toast.show({
type: ToastType.Info,
autoHide: false,
text1: "Checking internet reachable",
})
*/
} else if (!hasShown) {
_showNetworkWarning()
}
})

// Strict check for false, null means the network state is not yet known
if (internetReachable === false && !hasShown) {
showNetworkWarning()
}
}, [
isConnected,
disableMediatorCheck,
showNetworkWarning,
assertInternetReachable,
assertMediatorReachable,
t,
hasShown
])

Expand Down
1 change: 0 additions & 1 deletion packages/legacy/core/App/container-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export const defaultConfig: Config = {
showPreface: false,
disableOnboardingSkip: false,
disableContactsInSettings: false,
disableMediatorCheck: false,
internetReachabilityUrls: ['https://clients3.google.com/generate_204'],
whereToUseWalletUrl: 'https://example.com',
showScanHelp: true,
Expand Down
78 changes: 41 additions & 37 deletions packages/legacy/core/App/contexts/network.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,67 @@
import { NetInfoStateType, useNetInfo } from '@react-native-community/netinfo'
import * as React from 'react'
import { createContext, useContext, useState } from 'react'
import { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react'

import NetInfoModal from '../components/modals/NetInfoModal'
import { hostnameFromURL, canConnectToHost } from '../utils/network'
import { Config } from 'react-native-config'

export interface NetworkContext {
silentAssertConnectedNetwork: () => boolean
silentAssertConnectedNetwork: () => boolean | null
assertNetworkConnected: () => boolean
displayNetInfoModal: () => void
hideNetInfoModal: () => void
assertInternetReachable: () => Promise<boolean>
assertMediatorReachable: () => Promise<boolean>
assertInternetReachable: () => boolean | null
}

export const NetworkContext = createContext<NetworkContext>(null as unknown as NetworkContext)

export const NetworkProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const netInfo = useNetInfo()

// NOTE: @react-native-community/netinfo can be configured to use whichever reachability check desired
// eg. isInternetReachable can be set to check a specific URL (like your mediator). See the docs here for more info:
// https://github.com/react-native-netinfo/react-native-netinfo?tab=readme-ov-file#configure
export const NetworkProvider = ({ children }: PropsWithChildren) => {
const { isConnected, type, isInternetReachable } = useNetInfo()
const [isNetInfoModalDisplayed, setIsNetInfoModalDisplayed] = useState<boolean>(false)

const displayNetInfoModal = () => {
const displayNetInfoModal = useCallback(() => {
setIsNetInfoModalDisplayed(true)
}
}, [])

const hideNetInfoModal = () => {
const hideNetInfoModal = useCallback(() => {
setIsNetInfoModalDisplayed(false)
}
}, [])

/**
* Returns null until the network state is known, then returns boolean
* Useful for cases where we do not want to take action until the network state is known
*
* @returns {boolean | null} - `true` if the network is connected, `false` if not connected,
* and `null` if the network status not yet known
*/
const silentAssertConnectedNetwork = useCallback((): boolean | null => {
return type === NetInfoStateType.unknown ? null : isConnected || [NetInfoStateType.wifi, NetInfoStateType.cellular].includes(type)
}, [isConnected, type])

const silentAssertConnectedNetwork = () => {
return netInfo.isConnected || [NetInfoStateType.wifi, NetInfoStateType.cellular].includes(netInfo.type)
}

const assertNetworkConnected = () => {
const isConnected = silentAssertConnectedNetwork()
if (!isConnected) {
/**
* Strictly asserts that the network is connected. Will return false even if
* the network state is not yet known - in this case it will also display the
* NetInfoModal
* Useful for cases where we must be sure of connectivity before proceeding
*
* @returns {boolean} - `true` if the network is checked and connected, otherwise `false`
*/
const assertNetworkConnected = useCallback(() => {
const connectionConfirmed = silentAssertConnectedNetwork() === true
if (!connectionConfirmed) {
displayNetInfoModal()
}
return isConnected
}

const assertInternetReachable = async (): Promise<boolean> => {
return netInfo.isInternetReachable as boolean
}

const assertMediatorReachable = async (): Promise<boolean> => {
const hostname = hostnameFromURL(Config.MEDIATOR_URL!)

if (hostname === null || hostname.length === 0) {
return false
}
return connectionConfirmed
}, [silentAssertConnectedNetwork, displayNetInfoModal])

const nodes = [{ host: hostname, port: 443 }]
const connections = await Promise.all(nodes.map((n: { host: string; port: number }) => canConnectToHost(n)))
const assertInternetReachable = useCallback((): boolean | null => {
return isInternetReachable
}, [isInternetReachable])

return connections.includes(true)
}

return (
<NetworkContext.Provider
Expand All @@ -65,8 +70,7 @@ export const NetworkProvider: React.FC<React.PropsWithChildren> = ({ children })
assertNetworkConnected,
displayNetInfoModal,
hideNetInfoModal,
assertInternetReachable,
assertMediatorReachable
assertInternetReachable
}}
>
{children}
Expand Down
1 change: 0 additions & 1 deletion packages/legacy/core/App/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export interface Config {
contactDetailsOptions?: ContactDetailsOptionsParams
credentialHideList?: string[]
disableContactsInSettings?: boolean
disableMediatorCheck?: boolean
internetReachabilityUrls: string[]
attemptLockoutConfig?: AttemptLockoutConfig
}
Expand Down
24 changes: 0 additions & 24 deletions packages/legacy/core/App/utils/network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,3 @@ export const fetchLedgerNodes = (indyNamespace = 'sovrin'): Array<{ host: string

return nodes
}

export const hostnameFromURL = (fullUrl: string): string | null => {
try {
// Start of the hostname after "//"
const startIndex = fullUrl.indexOf('//') + 2

// End of the hostname (before the next '/' or '?')
let endIndex = fullUrl.indexOf('/', startIndex)

// If no '/', look for '?'
if (endIndex === -1) {
endIndex = fullUrl.indexOf('?', startIndex)
}

// If no '/' or '?', hostname is till the end
if (endIndex === -1) {
endIndex = fullUrl.length
}

return fullUrl.substring(startIndex, endIndex)
} catch (error) {
return null
}
}
61 changes: 61 additions & 0 deletions packages/legacy/core/__tests__/components/NetInfo.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, waitFor } from '@testing-library/react-native'
import React from 'react'
import Toast from 'react-native-toast-message'

import mockNetworkContext from '../contexts/network'
import NetInfo from '../../App/components/network/NetInfo'
import toastConfig from '../../App/components/toast/ToastConfig'
import { BasicAppContext } from '../helpers/app'

// Bifold's top offset for toasts
const topOffset = 15

describe('NetInfo Component', () => {
it('should not show toast when internet is reachable', async () => {
mockNetworkContext.assertInternetReachable.mockReturnValue(true)

const { queryByText } = render(
<BasicAppContext>
<NetInfo />
<Toast topOffset={topOffset} config={toastConfig} />
</BasicAppContext>
)

await waitFor(async () => {
const toast = await queryByText('NetInfo.NoInternetConnectionTitle', { exact: false })
expect(toast).toBeNull()
})
})

it('should show toast when internet is not reachable', async () => {
mockNetworkContext.assertInternetReachable.mockReturnValue(false)

const { queryByText } = render(
<BasicAppContext>
<NetInfo />
<Toast topOffset={topOffset} config={toastConfig} />
</BasicAppContext>
)

await waitFor(async () => {
const toast = await queryByText('NetInfo.NoInternetConnectionTitle', { exact: false })
expect(toast).toBeTruthy()
})
})

it('should not show toast when internet reachability is unknown', async () => {
mockNetworkContext.assertInternetReachable.mockReturnValue(null)

const { queryByText } = render(
<BasicAppContext>
<NetInfo />
<Toast topOffset={topOffset} config={toastConfig} />
</BasicAppContext>
)

await waitFor(async () => {
const toast = await queryByText('NetInfo.NoInternetConnectionTitle', { exact: false })
expect(toast).toBeNull()
})
})
})
2 changes: 0 additions & 2 deletions packages/legacy/core/__tests__/contexts/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ const networkContext = {
silentAssertConnectedNetwork: jest.fn(),
displayNetInfoModal: jest.fn(),
hideNetInfoModal: jest.fn(),
// assertNetworkReachable: jest.fn(),
assertInternetReachable: jest.fn(),
assertMediatorReachable: jest.fn(),
}

export default networkContext

0 comments on commit d391221

Please sign in to comment.