Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Display backend unreachability message #38377

Merged
merged 33 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
37cf997
display backend unreachability message
tienifr Mar 15, 2024
4b24bd9
show status page hostname only
tienifr Mar 15, 2024
7e8cf7f
adjust polling timeout and status page link style
tienifr Mar 15, 2024
c7b627d
update comment
tienifr Mar 15, 2024
3da4432
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Mar 18, 2024
f3be100
update spanish copy
tienifr Mar 18, 2024
6fa6d52
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Mar 18, 2024
d3faf92
Update Espanol copy
tienifr Mar 18, 2024
8f84a24
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Mar 19, 2024
aae3f37
update comments
tienifr Mar 19, 2024
eb5094d
Merge branch 'main' into fix/37565
tienifr Mar 20, 2024
4c84f89
refactor: extract subscribeToBackendReachability and modify minor com…
tienifr Mar 20, 2024
0b5dec8
refactor: add returns doc
tienifr Mar 20, 2024
b7be2f2
refactor: return cleanup interval function
tienifr Mar 20, 2024
292656e
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Mar 29, 2024
ddc17c8
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Apr 1, 2024
4967911
remove redundant comment
tienifr Apr 1, 2024
4213ccf
fix typecheck
tienifr Apr 1, 2024
5c86046
rename constant
tienifr Apr 1, 2024
5de949b
Modify comment
tienifr Apr 2, 2024
3f1cf05
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Apr 5, 2024
2f8e345
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Apr 12, 2024
bffd5f0
implement internet reachability for android
tienifr Apr 12, 2024
01aafb0
modify fetch logic
tienifr Apr 12, 2024
c851a49
modify comment
tienifr Apr 12, 2024
045047f
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr Apr 23, 2024
17b6aad
Merge branch 'main' into fix/37565
tienifr May 2, 2024
3103489
set network status
tienifr May 2, 2024
ab835c9
Merge branch 'main' into fix/37565
tienifr May 6, 2024
3122072
fix isBackendReachable flickering
tienifr May 6, 2024
17a5965
modify comment
tienifr May 6, 2024
fb78adc
Merge branch 'main' of https://github.com/tienifr/App into fix/37565
tienifr May 8, 2024
ce38e99
Merge branch 'main' into fix/37565
tienifr May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import './fonts.css';
Onyx.init({
keys: ONYXKEYS,
initialKeyStates: {
[ONYXKEYS.NETWORK]: {isOffline: false},
[ONYXKEYS.NETWORK]: {isOffline: false, isBackendReachable: true},
},
});

Expand Down
5 changes: 4 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,10 @@ const CONST = {
EMPTY_ARRAY,
EMPTY_OBJECT,
USE_EXPENSIFY_URL,
STATUS_EXPENSIFY_URL: 'https://status.expensify.com',
GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com',
GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com',
GOOGLE_CLOUD_URL: 'https://clients3.google.com/generate_204',
IMAGE_BASE64_MATCH: 'base64',
DEEPLINK_BASE_URL: 'new-expensify://',
PDF_VIEWER_URL: '/pdf/web/viewer.html',
Expand Down Expand Up @@ -1045,6 +1047,7 @@ const CONST = {
MAX_RETRY_WAIT_TIME_MS: 10 * 1000,
PROCESS_REQUEST_DELAY_MS: 1000,
MAX_PENDING_TIME_MS: 10 * 1000,
BACKEND_CHECK_INTERVAL_MS: 60 * 1000,
MAX_REQUEST_RETRIES: 10,
NETWORK_STATUS: {
ONLINE: 'online',
Expand All @@ -1056,7 +1059,7 @@ const CONST = {
DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'},
DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false},
DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false},
DEFAULT_NETWORK_DATA: {isOffline: false},
DEFAULT_NETWORK_DATA: {isOffline: false, isBackendReachable: true},
FORMS: {
LOGIN_FORM: 'LoginForm',
VALIDATE_CODE_FORM: 'ValidateCodeForm',
Expand Down
6 changes: 4 additions & 2 deletions src/Expensify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,10 @@ function Expensify({
// Initialize this client as being an active client
ActiveClientManager.init();

// Used for the offline indicator appearing when someone is offline
NetworkConnection.subscribeToNetInfo();
// Used for the offline indicator appearing when someone is offline or backend is unreachable
const unsubscribeNetworkStatus = NetworkConnection.subscribeToNetworkStatus();

return () => unsubscribeNetworkStatus();
}, []);

useEffect(() => {
Expand Down
23 changes: 20 additions & 3 deletions src/components/OfflineIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import Text from './Text';
import TextLink from './TextLink';

type OfflineIndicatorProps = {
/** Optional styles for container element that will override the default styling for the offline indicator */
Expand All @@ -23,7 +25,7 @@ function OfflineIndicator({style, containerStyles}: OfflineIndicatorProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {isOffline, isBackendReachable} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();

const computedStyles = useMemo((): StyleProp<ViewStyle> => {
Expand All @@ -34,7 +36,7 @@ function OfflineIndicator({style, containerStyles}: OfflineIndicatorProps) {
return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator;
}, [containerStyles, isSmallScreenWidth, styles.offlineIndicatorMobile, styles.offlineIndicator]);

if (!isOffline) {
if (!isOffline && isBackendReachable) {
return null;
}

Expand All @@ -46,7 +48,22 @@ function OfflineIndicator({style, containerStyles}: OfflineIndicatorProps) {
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
/>
<Text style={[styles.ml3, styles.chatItemComposeSecondaryRowSubText]}>{translate('common.youAppearToBeOffline')}</Text>
<Text style={[styles.ml3, styles.chatItemComposeSecondaryRowSubText]}>
{isOffline ? (
translate('common.youAppearToBeOffline')
) : (
<>
{translate('common.weMightHaveProblem')}
<TextLink
href={CONST.STATUS_EXPENSIFY_URL}
style={[styles.chatItemComposeSecondaryRowSubText, styles.link]}
>
{new URL(CONST.STATUS_EXPENSIFY_URL).host}
</TextLink>
.
</>
)}
</Text>
</View>
);
}
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ type UseNetworkProps = {
onReconnect?: () => void;
};

type UseNetwork = {isOffline: boolean};
type UseNetwork = {isOffline: boolean; isBackendReachable: boolean};

export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {}): UseNetwork {
const callback = useRef(onReconnect);
callback.current = onReconnect;

const {isOffline, networkStatus} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN};
const {isOffline, networkStatus, isBackendReachable} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN};
const isNetworkStatusUnknown = networkStatus === CONST.NETWORK.NETWORK_STATUS.UNKNOWN;
const prevOfflineStatusRef = useRef(isOffline);
useEffect(() => {
// If we were offline before and now we are not offline then we just reconnected
Expand All @@ -29,6 +30,6 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {
prevOfflineStatusRef.current = isOffline;
}, [isOffline]);

// If the network status is undefined, we don't treat it as offline. Otherwise, we utilize the isOffline prop.
return {isOffline: networkStatus === CONST.NETWORK.NETWORK_STATUS.UNKNOWN ? false : isOffline};
// If the network status is unknown, we fallback to default state, i.e. we're online and backend is reachable.
return isNetworkStatusUnknown ? CONST.DEFAULT_NETWORK_DATA : {isOffline, isBackendReachable};
}
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export default {
your: 'your',
conciergeHelp: 'Please reach out to Concierge for help.',
youAppearToBeOffline: 'You appear to be offline.',
weMightHaveProblem: 'We might have a problem. Check out ',
thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.',
areYouSure: 'Are you sure?',
verify: 'Verify',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export default {
your: 'tu',
conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.',
youAppearToBeOffline: 'Parece que estás desconectado.',
weMightHaveProblem: 'Peude que te tengamos un problema. Echa un vistazo a ',
thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa para ser utilizada.',
areYouSure: '¿Estás seguro?',
verify: 'Verifique',
Expand Down
102 changes: 71 additions & 31 deletions src/libs/NetworkConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import * as NetworkActions from './actions/Network';
import AppStateMonitor from './AppStateMonitor';
import checkInternetReachability from './checkInternetReachability';
import Log from './Log';

let isOffline = false;
Expand Down Expand Up @@ -42,12 +43,23 @@ function setOfflineStatus(isCurrentlyOffline: boolean): void {
// When reconnecting, ie, going from offline to online, all the reconnection callbacks
// are triggered (this is usually Actions that need to re-download data from the server)
if (isOffline && !isCurrentlyOffline) {
NetworkActions.setIsBackendReachable(true);
triggerReconnectionCallbacks('offline status changed');
}

isOffline = isCurrentlyOffline;
}

function setNetWorkStatus(isInternetReachable: boolean | null): void {
let networkStatus;
if (!isBoolean(isInternetReachable)) {
networkStatus = CONST.NETWORK.NETWORK_STATUS.UNKNOWN;
} else {
networkStatus = isInternetReachable ? CONST.NETWORK.NETWORK_STATUS.ONLINE : CONST.NETWORK.NETWORK_STATUS.OFFLINE;
}
NetworkActions.setNetWorkStatus(networkStatus);
}

// Update the offline status in response to changes in shouldForceOffline
let shouldForceOffline = false;
Onyx.connect({
Expand All @@ -71,53 +83,81 @@ Onyx.connect({
});

/**
* Set up the event listener for NetInfo to tell whether the user has
* internet connectivity or not. This is more reliable than the Pusher
* `disconnected` event which takes about 10-15 seconds to emit.
* Set interval to periodically (re)check backend status.
* Because backend unreachability might imply lost internet connection, we need to check internet reachability.
* @returns clearInterval cleanup
*/
function subscribeToNetInfo(): void {
// Note: We are disabling the configuration for NetInfo when using the local web API since requests can get stuck in a 'Pending' state and are not reliable indicators for "offline".
// If you need to test the "recheck" feature then switch to the production API proxy server.
if (!CONFIG.IS_USING_LOCAL_WEB) {
// Calling NetInfo.configure (re)checks current state. We use it to force a recheck whenever we (re)subscribe
NetInfo.configure({
// By default, NetInfo uses `/` for `reachabilityUrl`
// When App is served locally (or from Electron) this address is always reachable - even offline
// Using the API url ensures reachability is tested over internet
reachabilityUrl: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/Ping`,
reachabilityMethod: 'GET',
reachabilityTest: (response) => {
function subscribeToBackendAndInternetReachability(): () => void {
const intervalID = setInterval(() => {
// Offline status also implies backend unreachability
if (isOffline) {
return;
}
// Using the API url ensures reachability is tested over internet
fetch(`${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/Ping`, {
method: 'GET',
cache: 'no-cache',
})
.then((response) => {
if (!response.ok) {
return Promise.resolve(false);
}
return response
.json()
.then((json) => Promise.resolve(json.jsonCode === 200))
.catch(() => Promise.resolve(false));
},
})
.then((isBackendReachable: boolean) => {
if (isBackendReachable) {
NetworkActions.setIsBackendReachable(true);
return;
}
checkInternetReachability().then((isInternetReachable: boolean) => {
setOfflineStatus(!isInternetReachable);
setNetWorkStatus(isInternetReachable);
NetworkActions.setIsBackendReachable(false);
});
})
.catch(() => {
checkInternetReachability().then((isInternetReachable: boolean) => {
setOfflineStatus(!isInternetReachable);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tienifr We implemented subscribeToNetworkStatus to detect the onl/off status by using NetInfo.addEventListener. Why do we need to call setOfflineStatus here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DylanDylann The reason for this check is here: #38377 (comment). Please raise questions if any.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that you might wonder whether react-native-netinfo's event listener collided with our own custom checkInternetReachability. Note that we only run this check on Android and we do not trigger it if the system is already offline here.

setNetWorkStatus(isInternetReachable);
NetworkActions.setIsBackendReachable(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this inside the promise for checkInternetReachability()? It doesn't do anything with the isInternetReachable value.

Copy link
Contributor Author

@tienifr tienifr Jun 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You meant the setIsBackendReachable, didn't you?

Backend unreachability might mean internet unreachability so we need to check the internet first to clarify whether the root cause is internet or backend failure.

If we move the setIsBackendReachable out of the promise, we would have We might have problem ... appear briefly before You appear to be offline when the internet was down.

});
});
}, CONST.NETWORK.BACKEND_CHECK_INTERVAL_MS);

return () => {
clearInterval(intervalID);
};
}

// If a check is taking longer than this time we're considered offline
reachabilityRequestTimeout: CONST.NETWORK.MAX_PENDING_TIME_MS,
});
}
/**
* Monitor internet connectivity and perform periodic backend reachability checks
* @returns unsubscribe method
*/
function subscribeToNetworkStatus(): () => void {
// Note: We are disabling the reachability check when using the local web API since requests can get stuck in a 'Pending' state and are not reliable indicators for reachability.
// If you need to test the "recheck" feature then switch to the production API proxy server.
const unsubscribeFromBackendReachability = !CONFIG.IS_USING_LOCAL_WEB ? subscribeToBackendAndInternetReachability() : undefined;

// Subscribe to the state change event via NetInfo so we can update
// whether a user has internet connectivity or not.
NetInfo.addEventListener((state) => {
// Set up the event listener for NetInfo to tell whether the user has
// internet connectivity or not. This is more reliable than the Pusher
// `disconnected` event which takes about 10-15 seconds to emit.
const unsubscribeNetInfo = NetInfo.addEventListener((state) => {
tienifr marked this conversation as resolved.
Show resolved Hide resolved
Log.info('[NetworkConnection] NetInfo state change', false, {...state});
if (shouldForceOffline) {
Log.info('[NetworkConnection] Not setting offline status because shouldForceOffline = true');
return;
}
setOfflineStatus((state.isInternetReachable ?? false) === false);
let networkStatus;
if (!isBoolean(state.isInternetReachable)) {
networkStatus = CONST.NETWORK.NETWORK_STATUS.UNKNOWN;
} else {
networkStatus = state.isInternetReachable ? CONST.NETWORK.NETWORK_STATUS.ONLINE : CONST.NETWORK.NETWORK_STATUS.OFFLINE;
}
NetworkActions.setNetWorkStatus(networkStatus);
setNetWorkStatus(state.isInternetReachable);
});

return () => {
unsubscribeFromBackendReachability?.();
unsubscribeNetInfo();
};
}

function listenForReconnect() {
Expand Down Expand Up @@ -166,6 +206,6 @@ export default {
onReconnect,
triggerReconnectionCallbacks,
recheckNetworkConnection,
subscribeToNetInfo,
subscribeToNetworkStatus,
};
export type {NetworkStatus};
6 changes: 5 additions & 1 deletion src/libs/actions/Network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Onyx from 'react-native-onyx';
import type {NetworkStatus} from '@libs/NetworkConnection';
import ONYXKEYS from '@src/ONYXKEYS';

function setIsBackendReachable(isBackendReachable: boolean) {
Onyx.merge(ONYXKEYS.NETWORK, {isBackendReachable});
}

function setIsOffline(isOffline: boolean) {
Onyx.merge(ONYXKEYS.NETWORK, {isOffline});
}
Expand All @@ -25,4 +29,4 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) {
Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests});
}

export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus};
export {setIsBackendReachable, setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus};
15 changes: 15 additions & 0 deletions src/libs/checkInternetReachability/index.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import CONST from '@src/CONST';
import type InternetReachabilityCheck from './types';

/**
* Although Android supports internet reachability check, it only does on initiating the connection.
* We need to implement a test for a highly-available endpoint in case of lost internet after initiation.
*/
export default function checkInternetReachability(): InternetReachabilityCheck {
return fetch(CONST.GOOGLE_CLOUD_URL, {
method: 'GET',
cache: 'no-cache',
})
.then((response) => Promise.resolve(response.status === 204))
.catch(() => Promise.resolve(false));
}
5 changes: 5 additions & 0 deletions src/libs/checkInternetReachability/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type InternetReachabilityCheck from './types';

export default function checkInternetReachability(): InternetReachabilityCheck {
return Promise.resolve(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a no-op for all other platforms? Shouldn't this at least be doing a NetInfo.fetch()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need to manually check for internet reachability on Android. This is due to a limitation on Android OS:

/**
* Although Android supports internet reachability check, it only does on initiating the connection.
* We need to implement a test for a highly-available endpoint in case of lost internet after initiation.
*/

Other platforms does not have that problem so we use NetInfo's own check:

const unsubscribeNetInfo = NetInfo.addEventListener((state) => {

}
3 changes: 3 additions & 0 deletions src/libs/checkInternetReachability/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type InternetReachabilityCheck = Promise<boolean>;

export default InternetReachabilityCheck;
3 changes: 3 additions & 0 deletions src/types/onyx/Network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ type Network = {
/** Is the network currently offline or not */
isOffline: boolean;

/** Is the backend reachable when online */
isBackendReachable: boolean;

/** Should the network be forced offline */
shouldForceOffline?: boolean;

Expand Down
Loading
Loading