diff --git a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap index 0d463bf8844..494d577fb9f 100644 --- a/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap +++ b/ts/features/common/store/reducers/__tests__/__snapshots__/index.test.ts.snap @@ -2,6 +2,9 @@ exports[`featuresPersistor should match snapshot 1`] = ` { + "connectivityStatus": { + "isConnected": true, + }, "fci": { "documentPreview": { "kind": "PotNone", diff --git a/ts/features/common/store/reducers/index.ts b/ts/features/common/store/reducers/index.ts index d9bf2ba71ba..7a422883f2e 100644 --- a/ts/features/common/store/reducers/index.ts +++ b/ts/features/common/store/reducers/index.ts @@ -60,6 +60,9 @@ import { spidLoginReducer, SpidLoginState } from "../../../spidLogin/store/reducers"; +import connectivityStateReducer, { + ConnectivityState +} from "../../../connectivity/store/reducers"; import { GlobalState } from "../../../../store/reducers/types"; import { isIOMarkdownDisabledForMessagesAndServices } from "../../../../store/reducers/backendStatus/remoteConfig"; import { isIOMarkdownEnabledLocallySelector } from "../../../../store/reducers/persistedPreferences"; @@ -93,6 +96,7 @@ export type FeaturesState = { mixpanel: MixpanelState; ingress: IngressScreenState; landingBanners: LandingScreenBannerState; + connectivityStatus: ConnectivityState; }; export type PersistedFeaturesState = FeaturesState & PersistPartial; @@ -119,7 +123,8 @@ const rootReducer = combineReducers({ profileSettings: profileSettingsReducerPersistor, mixpanel: mixpanelReducer, ingress: ingressScreenReducer, - landingBanners: landingScreenBannersReducer + landingBanners: landingScreenBannersReducer, + connectivityStatus: connectivityStateReducer }); const CURRENT_REDUX_FEATURES_STORE_VERSION = 1; diff --git a/ts/features/connectivity/saga/index.ts b/ts/features/connectivity/saga/index.ts new file mode 100644 index 00000000000..a83752f5b45 --- /dev/null +++ b/ts/features/connectivity/saga/index.ts @@ -0,0 +1,66 @@ +import * as E from "fp-ts/lib/Either"; +import { call, fork, put, select } from "typed-redux-saga/macro"; +import { Millisecond } from "@pagopa/ts-commons/lib/units"; +import { configureNetInfo, fetchNetInfoState } from "../utils"; +import { startTimer } from "../../../utils/timer"; +import { setConnectionStatus } from "../store/actions"; +import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; +import { isConnectedSelector } from "../store/selectors"; + +const CONNECTIVITY_STATUS_LOAD_INTERVAL = (60 * 1000) as Millisecond; +const CONNECTIVITY_STATUS_FAILURE_INTERVAL = (10 * 1000) as Millisecond; + +/** + * this saga requests and checks the connection status + */ +export function* connectionStatusSaga(): Generator< + ReduxSagaEffect, + boolean, + SagaCallReturnType +> { + try { + const response = yield* call(fetchNetInfoState()); + if (E.isRight(response)) { + yield* put(setConnectionStatus(response.right.isConnected === true)); + return true; + } + return false; + } catch (e) { + // do nothing. it should be a library error + return false; + } +} + +/** + * this saga requests and checks in loop connection status + * if the connection is off app could show a warning message or avoid + * the whole usage. + */ +export function* connectionStatusWatcherLoop() { + // check connectivity status periodically + while (true) { + const response: SagaCallReturnType = + yield* call(connectionStatusSaga); + + // if we have no connection increase rate + if (response === false) { + yield* call(startTimer, CONNECTIVITY_STATUS_FAILURE_INTERVAL); + continue; + } + + const isAppConnected = yield* select(isConnectedSelector); + + // if connection is off increase rate + if (!isAppConnected) { + yield* call(startTimer, CONNECTIVITY_STATUS_FAILURE_INTERVAL); + } else { + yield* call(startTimer, CONNECTIVITY_STATUS_LOAD_INTERVAL); + } + } +} + +export default function* root(): IterableIterator { + // configure net info library to check status and fetch a specific url + configureNetInfo(); + yield* fork(connectionStatusWatcherLoop); +} diff --git a/ts/features/connectivity/store/actions/index.ts b/ts/features/connectivity/store/actions/index.ts new file mode 100644 index 00000000000..333458462a7 --- /dev/null +++ b/ts/features/connectivity/store/actions/index.ts @@ -0,0 +1,13 @@ +import { ActionType, createStandardAction } from "typesafe-actions"; + +export const requestConnectionStatus = createStandardAction( + "REQUEST_CONNECTION_STATUS" +)(); + +export const setConnectionStatus = createStandardAction( + "SET_CONNECTION_STATUS" +)(); + +export type ConnectivityActions = + | ActionType + | ActionType; diff --git a/ts/features/connectivity/store/reducers/index.ts b/ts/features/connectivity/store/reducers/index.ts new file mode 100644 index 00000000000..6544b6ea967 --- /dev/null +++ b/ts/features/connectivity/store/reducers/index.ts @@ -0,0 +1,31 @@ +import { getType } from "typesafe-actions"; +import { Action } from "../../../../store/actions/types"; +import { setConnectionStatus } from "../actions"; + +// Define the type for the connection state +export type ConnectivityState = { + isConnected: boolean; +}; + +// Define the initial state +const initialState: ConnectivityState = { + isConnected: true +}; + +// Define the reducer +const connectivityStateReducer = ( + state: ConnectivityState = initialState, + action: Action +): ConnectivityState => { + switch (action.type) { + case getType(setConnectionStatus): + return { + ...state, + isConnected: action.payload + }; + default: + return state; + } +}; + +export default connectivityStateReducer; diff --git a/ts/features/connectivity/store/selectors/index.ts b/ts/features/connectivity/store/selectors/index.ts new file mode 100644 index 00000000000..af6a63aa8aa --- /dev/null +++ b/ts/features/connectivity/store/selectors/index.ts @@ -0,0 +1,7 @@ +import { GlobalState } from "../../../../store/reducers/types"; + +export const connectivityStatusSelector = (state: GlobalState) => + state.features.connectivityStatus; + +export const isConnectedSelector = (state: GlobalState) => + state.features.connectivityStatus.isConnected; diff --git a/ts/features/connectivity/utils/index.ts b/ts/features/connectivity/utils/index.ts new file mode 100644 index 00000000000..dae8ddd62ac --- /dev/null +++ b/ts/features/connectivity/utils/index.ts @@ -0,0 +1,23 @@ +import { configure, fetch } from "@react-native-community/netinfo"; +import { pipe } from "fp-ts/lib/function"; +import * as TE from "fp-ts/lib/TaskEither"; +import { apiUrlPrefix } from "../../../config"; + +const IO_REACHABIITY_URL = `${apiUrlPrefix}/api/v1/status`; + +export const configureNetInfo = () => + configure({ + reachabilityUrl: IO_REACHABIITY_URL, + useNativeReachability: false, + reachabilityRequestTimeout: 15000, + reachabilityLongTimeout: 60000, + reachabilityShortTimeout: 5000 + }); + +export const fetchNetInfoState = () => + pipe( + TE.tryCatch( + () => fetch(), + error => new Error(`Error fetching net info state: ${error}`) + ) + ); diff --git a/ts/sagas/index.ts b/ts/sagas/index.ts index 5088716af60..93a3896eee5 100644 --- a/ts/sagas/index.ts +++ b/ts/sagas/index.ts @@ -7,6 +7,7 @@ import { watchTokenRefreshSaga } from "../features/fastLogin/saga/tokenRefreshSa import { watchPendingActionsSaga } from "../features/fastLogin/saga/pendingActionsSaga"; import { watchZendeskSupportSaga } from "../features/zendesk/saga"; import { zendeskEnabled } from "../config"; +import connectivityStatusSaga from "../features/connectivity/saga"; import backendStatusSaga from "./backendStatus"; import { watchContentSaga } from "./contentLoaders"; import { loadSystemPreferencesSaga } from "./preferences"; @@ -22,6 +23,7 @@ export default function* root() { call(watchApplicationActivitySaga), call(startupSaga), call(backendStatusSaga), + call(connectivityStatusSaga), call(versionInfoSaga), call(loadSystemPreferencesSaga), call(removePersistedStatesSaga), diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index eba0c72243f..a6630a5930c 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -35,6 +35,7 @@ import { IngressScreenActions } from "../../features/ingress/store/actions"; import { MixpanelFeatureActions } from "../../features/mixpanel/store/actions"; import { LandingScreenBannerActions } from "../../features/landingScreenMultiBanner/store/actions"; import { SpidConfigActions } from "../../features/spidLogin/store/actions"; +import { ConnectivityActions } from "../../features/connectivity/store/actions"; import { LoginPreferencesActions } from "../../features/login/preferences/store/actions"; import { AnalyticsActions } from "./analytics"; import { ApplicationActions } from "./application"; @@ -109,6 +110,7 @@ export type Action = | MixpanelFeatureActions | LandingScreenBannerActions | SpidConfigActions + | ConnectivityActions | LoginPreferencesActions; export type Dispatch = DispatchAPI;