diff --git a/src/actionTypes.js b/src/actionTypes.js index ac0136a0546..4012b62e32b 100644 --- a/src/actionTypes.js +++ b/src/actionTypes.js @@ -117,7 +117,7 @@ type RehydrateAction = {| type AppOnlineAction = {| type: typeof APP_ONLINE, - isOnline: boolean, + isOnline: boolean | null, |}; type DeadQueueAction = {| diff --git a/src/boot/AppEventHandlers.js b/src/boot/AppEventHandlers.js index 303ff024102..2e1fedb969f 100644 --- a/src/boot/AppEventHandlers.js +++ b/src/boot/AppEventHandlers.js @@ -65,9 +65,49 @@ class AppEventHandlersInner extends PureComponent { handleConnectivityChange = netInfoState => { const { dispatch } = this.props; - const { type: connectionType } = netInfoState; - const isConnected = connectionType !== 'none' && connectionType !== 'unknown'; - dispatch(appOnline(isConnected)); + + dispatch( + appOnline( + // From reading code at @react-native-community/net-info v6.0.0 (the + // docs and types don't really give these answers): + // + // This will be `null` on both platforms while the first known value + // of `true` or `false` is being shipped across the asynchronous RN + // bridge. + // + // On Android, it shouldn't otherwise be `null`. The value is set to the + // result of an Android function that only returns a boolean: + // https://developer.android.com/reference/android/net/NetworkInfo#isConnected() + // + // On iOS, this can also be `null` while the app asynchronously + // evaluates whether a network change should cause this to go from + // `false` to `true`. Read on for details (gathered from + // src/internal/internetReachability.ts in the library). + // + // 1. A request loop is started. A HEAD request is made to + // https://clients3.google.com/generate_204, with a timeout of + // 15s, to see if the Internet is reachable. + // - If the `fetch` succeeds and a 204 is received, this will be + // made `true`. We'll then sleep for 60s before making the + // request again. + // - If the `fetch` succeeds and a 204 is not received, or if the + // fetch fails, or if the timeout expires, this will be made + // `false`. We'll then sleep for only 5s before making the + // request again. + // 2. The request loop is interrupted if we get a + // 'netInfo.networkStatusDidChange' event from the library's + // native code, signaling a change in the network state. If that + // change would make `netInfoState.type` become or remain + // something good (i.e., not 'none' or 'unknown'), and this + // (`.isInternetReachable`) is currently `false`, then this will + // be made `null`, and the request loop described above will + // start again. + // + // (Several of those parameters are configurable -- timeout durations, + // URL, etc.) + netInfoState.isInternetReachable, + ), + ); }; /** For the type, see docs: https://reactnative.dev/docs/appstate */ diff --git a/src/common/OfflineNotice.js b/src/common/OfflineNotice.js index f72dd33da43..8f3ba3296a4 100644 --- a/src/common/OfflineNotice.js +++ b/src/common/OfflineNotice.js @@ -30,7 +30,7 @@ type Props = $ReadOnly<{||}>; */ export default function OfflineNotice(props: Props): Node { const isOnline = useSelector(state => getSession(state).isOnline); - if (isOnline) { + if (isOnline === true || isOnline === null) { return null; } diff --git a/src/directSelectors.js b/src/directSelectors.js index 1292fc8158a..4ac87dbced9 100644 --- a/src/directSelectors.js +++ b/src/directSelectors.js @@ -31,7 +31,7 @@ export const getAccounts = (state: GlobalState): Account[] => state.accounts; export const getSession = (state: GlobalState): SessionState => state.session; -export const getIsOnline = (state: GlobalState): boolean => state.session.isOnline; +export const getIsOnline = (state: GlobalState): boolean | null => state.session.isOnline; export const getDebug = (state: GlobalState): Debug => state.session.debug; export const getIsHydrated = (state: GlobalState): boolean => state.session.isHydrated; diff --git a/src/session/sessionActions.js b/src/session/sessionActions.js index e1b38128251..93986f7308c 100644 --- a/src/session/sessionActions.js +++ b/src/session/sessionActions.js @@ -8,7 +8,7 @@ import { DISMISS_SERVER_COMPAT_NOTICE, } from '../actionConstants'; -export const appOnline = (isOnline: boolean): Action => ({ +export const appOnline = (isOnline: boolean | null): Action => ({ type: APP_ONLINE, isOnline, }); diff --git a/src/session/sessionReducer.js b/src/session/sessionReducer.js index 55cf4657e00..318efced3a6 100644 --- a/src/session/sessionReducer.js +++ b/src/session/sessionReducer.js @@ -29,7 +29,11 @@ import { getHasAuth } from '../account/accountsSelectors'; */ export type SessionState = {| eventQueueId: number, - isOnline: boolean, + + // `null` if we don't know. See the place where we set this, for what that + // means. + isOnline: boolean | null, + isHydrated: boolean, /** @@ -79,7 +83,11 @@ export type SessionState = {| const initialState: SessionState = { eventQueueId: -1, - isOnline: true, + + // This will be `null` on startup, while we wait to hear `true` or `false` + // from the native module over the RN bridge; so, have it start as `null`. + isOnline: null, + isHydrated: false, loading: false, needsInitialFetch: false,