ts/sagas/startup.ts
import * as pot from "@pagopa/ts-commons/lib/pot";
import { Millisecond } from "@pagopa/ts-commons/lib/units";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { Alert } from "react-native";
import { channel } from "redux-saga";
import {
call,
cancel,
delay,
fork,
put,
select,
spawn,
take,
takeEvery,
takeLatest
} from "typed-redux-saga/macro";
import { ActionType, getType } from "typesafe-actions";
import { UserDataProcessingChoiceEnum } from "../../definitions/backend/UserDataProcessingChoice";
import { UserDataProcessingStatusEnum } from "../../definitions/backend/UserDataProcessingStatus";
import { BackendClient } from "../api/backend";
import {
apiUrlPrefix,
cdcEnabled,
euCovidCertificateEnabled,
pagoPaApiUrlPrefix,
pagoPaApiUrlPrefixTest,
zendeskEnabled
} from "../config";
import { watchBonusCdcSaga } from "../features/bonus/cdc/saga";
import { watchBonusCgnSaga } from "../features/bonus/cgn/saga";
import { watchEUCovidCertificateSaga } from "../features/euCovidCert/saga";
import { setSecurityAdviceReadyToShow } from "../features/fastLogin/store/actions/securityAdviceActions";
import { refreshSessionToken } from "../features/fastLogin/store/actions/tokenRefreshActions";
import {
isFastLoginEnabledSelector,
tokenRefreshSelector
} from "../features/fastLogin/store/selectors";
import { watchFciSaga } from "../features/fci/saga";
import { watchIDPaySaga } from "../features/idpay/common/saga";
import { checkPublicKeyAndBlockIfNeeded } from "../features/lollipop/navigation";
import {
checkLollipopSessionAssertionAndInvalidateIfNeeded,
generateLollipopKeySaga,
getKeyInfo
} from "../features/lollipop/saga";
import { lollipopPublicKeySelector } from "../features/lollipop/store/reducers/lollipop";
import { watchMessagesSaga } from "../features/messages/saga";
import { handleClearAllAttachments } from "../features/messages/saga/handleClearAttachments";
import { watchPnSaga } from "../features/pn/store/sagas/watchPnSaga";
import { watchPaymentsSaga } from "../features/payments/common/saga";
import {
watchGetZendeskTokenSaga,
watchZendeskGetSessionSaga
} from "../features/zendesk/saga";
import I18n from "../i18n";
import { mixpanelTrack } from "../mixpanel";
import NavigationService from "../navigation/NavigationService";
import {
applicationInitialized,
startApplicationInitialization
} from "../store/actions/application";
import { sessionExpired } from "../store/actions/authentication";
import { backendStatusLoadSuccess } from "../store/actions/backendStatus";
import { differentProfileLoggedIn } from "../store/actions/crossSessions";
import { previousInstallationDataDeleteSuccess } from "../store/actions/installation";
import { setMixpanelEnabled } from "../store/actions/mixpanel";
import { navigateToPrivacyScreen } from "../store/actions/navigation";
import { clearOnboarding } from "../store/actions/onboarding";
import { clearCache, resetProfileState } from "../store/actions/profile";
import {
startupLoadSuccess,
startupTransientError
} from "../store/actions/startup";
import { loadUserDataProcessing } from "../store/actions/userDataProcessing";
import {
sessionInfoSelector,
sessionTokenSelector
} from "../store/reducers/authentication";
import {
backendStatusSelector,
isPnEnabledSelector,
isSettingsVisibleAndHideProfileSelector
} from "../store/reducers/backendStatus";
import { IdentificationResult } from "../store/reducers/identification";
import {
isIdPayTestEnabledSelector,
isPagoPATestEnabledSelector
} from "../store/reducers/persistedPreferences";
import {
isProfileFirstOnBoarding,
profileSelector
} from "../store/reducers/profile";
import {
StartupStatusEnum,
startupTransientErrorInitialState
} from "../store/reducers/startup";
import { ReduxSagaEffect, SagaCallReturnType } from "../types/utils";
import { trackKeychainFailures } from "../utils/analytics";
import { isTestEnv } from "../utils/environment";
import { walletPaymentHandlersInitialized } from "../store/actions/wallet/payment";
import { watchFimsSaga } from "../features/fims/common/saga";
import { deletePin, getPin } from "../utils/keychain";
import { watchEmailValidationSaga } from "../store/sagas/emailValidationPollingSaga";
import { handleIsKeyStrongboxBacked } from "../features/lollipop/utils/crypto";
import { watchWalletSaga as watchNewWalletSaga } from "../features/newWallet/saga";
import { watchServicesSaga } from "../features/services/common/saga";
import { watchItwSaga } from "../features/itwallet/common/saga";
import { watchTrialSystemSaga } from "../features/trialSystem/store/sagas/watchTrialSystemSaga";
import { notificationPermissionsListener } from "../features/pushNotifications/sagas/notificationPermissionsListener";
import { profileAndSystemNotificationsPermissions } from "../features/pushNotifications/sagas/profileAndSystemNotificationsPermissions";
import { pushNotificationTokenUpload } from "../features/pushNotifications/sagas/pushNotificationTokenUpload";
import { handlePendingMessageStateIfAllowed } from "../features/pushNotifications/sagas/common";
import { cancellAllLocalNotifications } from "../features/pushNotifications/utils";
import { handleApplicationStartupTransientError } from "../features/startup/sagas";
import { formatRequestedTokenString } from "../features/zendesk/utils";
import { isBlockingScreenSelector } from "../features/ingress/store/selectors";
import { startAndReturnIdentificationResult } from "./identification";
import { previousInstallationDataDeleteSaga } from "./installation";
import {
askMixpanelOptIn,
handleSetMixpanelEnabled,
initMixpanel,
watchForActionsDifferentFromRequestLogoutThatMustResetMixpanel
} from "./mixpanel";
import { setLanguageFromProfileIfExists } from "./preferences";
import {
loadProfile,
watchProfile,
watchProfileRefreshRequestsSaga,
watchProfileUpsertRequestsSaga
} from "./profile";
import { askServicesPreferencesModeOptin } from "./services/servicesOptinSaga";
import { checkAppHistoryVersionSaga } from "./startup/appVersionHistorySaga";
import { authenticationSaga } from "./startup/authenticationSaga";
import { checkAcceptedTosSaga } from "./startup/checkAcceptedTosSaga";
import { checkAcknowledgedEmailSaga } from "./startup/checkAcknowledgedEmailSaga";
import { checkConfiguredPinSaga } from "./startup/checkConfiguredPinSaga";
import { watchEmailNotificationPreferencesSaga } from "./startup/checkEmailNotificationPreferencesSaga";
import { checkEmailSaga } from "./startup/checkEmailSaga";
import { checkProfileEnabledSaga } from "./startup/checkProfileEnabledSaga";
import { completeOnboardingSaga } from "./startup/completeOnboardingSaga";
import { loadSessionInformationSaga } from "./startup/loadSessionInformationSaga";
import { checkAcknowledgedFingerprintSaga } from "./startup/onboarding/biometric/checkAcknowledgedFingerprintSaga";
import { watchAbortOnboardingSaga } from "./startup/watchAbortOnboardingSaga";
import {
checkSession,
watchCheckSessionSaga
} from "./startup/watchCheckSessionSaga";
import { watchLogoutSaga } from "./startup/watchLogoutSaga";
import { watchSessionExpiredSaga } from "./startup/watchSessionExpiredSaga";
import { checkItWalletIdentitySaga } from "./startup/checkItWalletIdentitySaga";
import { watchUserDataProcessingSaga } from "./user/userDataProcessing";
import { watchWalletSaga } from "./wallet";
import { watchProfileEmailValidationChangedSaga } from "./watchProfileEmailValidationChangedSaga";
export const WAIT_INITIALIZE_SAGA = 5000 as Millisecond;
const navigatorPollingTime = 125 as Millisecond;
const warningWaitNavigatorTime = 2000 as Millisecond;
/**
* Handles the application startup and the main application logic loop
*/
// eslint-disable-next-line sonarjs/cognitive-complexity, complexity
export function* initializeApplicationSaga(
startupAction?: ActionType<typeof startApplicationInitialization>
): Generator<ReduxSagaEffect, void, any> {
const isBlockingScreen = yield* select(isBlockingScreenSelector);
if (isBlockingScreen) {
return;
}
const handleSessionExpiration = !!(
startupAction?.payload && startupAction.payload.handleSessionExpiration
);
const showIdentificationModal =
startupAction?.payload?.showIdentificationModalAtStartup ?? true;
// Remove explicitly previous session data. This is done as completion of two
// use cases:
// 1. Logout with data reset
// 2. FIXME: as a workaround for iOS only. Below iOS version 12.3 Keychain is
// not cleared between one installation and another, so it is
// needed to manually clear previous installation user info in
// order to force the user to choose unlock code and run through onboarding
// every new installation.
// store the app version in the history, if the current version is not present
yield* call(checkAppHistoryVersionSaga);
// check if mixpanel could be initialized
yield* call(initMixpanel);
yield* call(waitForNavigatorServiceInitialization);
// remove all local notifications (see function comment)
yield* call(cancellAllLocalNotifications);
yield* call(previousInstallationDataDeleteSaga);
yield* put(previousInstallationDataDeleteSuccess());
// listen for mixpanel enabling events
yield* takeLatest(setMixpanelEnabled, handleSetMixpanelEnabled);
// clear cached downloads when the logged user changes
yield* takeEvery(differentProfileLoggedIn, handleClearAllAttachments);
// Retrieve and listen for notification permissions status changes
yield* fork(notificationPermissionsListener);
// Get last logged in Profile from the state
const lastLoggedInProfileState: ReturnType<typeof profileSelector> =
yield* select(profileSelector);
const lastEmailValidated = pot.isSome(lastLoggedInProfileState)
? O.fromNullable(lastLoggedInProfileState.value.is_email_validated)
: O.none;
// Watch for profile changes
yield* fork(watchProfileEmailValidationChangedSaga, lastEmailValidated);
// Reset the profile cached in redux: at each startup we want to load a fresh
// user profile.
if (!handleSessionExpiration) {
yield* put(resetProfileState());
}
// We need to generate a key in the application startup flow
// to use this information on old app version already logged in users.
// Here we are blocking the application startup, but we have the
// the profile loading spinner active.
yield* call(generateLollipopKeySaga);
// This saga must retrieve the publicKey by its own,
// since it must make sure to have the latest in-memory value
// (as an example, during the authentication saga the key may have been regenerated multiple times)
// #LOLLIPOP_CHECK_BLOCK1_START
const unsupportedDevice = yield* call(checkPublicKeyAndBlockIfNeeded);
if (unsupportedDevice) {
return;
}
// #LOLLIPOP_CHECK_BLOCK1_END
// Since the backend.json is done in parallel with the startup saga,
// we need to synchronize the two tasks, to be sure to have loaded the remote FF
// before using them.
const backendStatus = yield* select(backendStatusSelector);
if (O.isNone(backendStatus)) {
yield* take(backendStatusLoadSuccess);
}
// Whether the user is currently logged in.
const previousSessionToken: ReturnType<typeof sessionTokenSelector> =
yield* select(sessionTokenSelector);
// workaround to send keychainError
// TODO: REMOVE AFTER FIXING https://pagopa.atlassian.net/jira/software/c/projects/IABT/boards/92?modal=detail&selectedIssue=IABT-1441
yield* call(trackKeychainFailures);
// Unless we have a valid session token already, login until we have one.
const sessionToken: SagaCallReturnType<typeof authenticationSaga> =
previousSessionToken
? previousSessionToken
: yield* call(authenticationSaga);
// BE CAREFUL where you get lollipop keyInfo.
// They MUST be placed after authenticationSaga, because they are regenerated with each login attempt.
// Get keyInfo for lollipop
const keyInfo = yield* call(getKeyInfo);
// Handles the expiration of the session token
yield* fork(watchSessionExpiredSaga);
yield* fork(watchForActionsDifferentFromRequestLogoutThatMustResetMixpanel);
// Instantiate a backend client from the session token
const backendClient: ReturnType<typeof BackendClient> = BackendClient(
apiUrlPrefix,
sessionToken,
keyInfo
);
// Watch for requests to logout
// Since this saga is spawned and not forked
// it will handle its own cancelation logic.
yield* spawn(watchLogoutSaga, backendClient.logout);
if (zendeskEnabled) {
yield* fork(watchZendeskGetSessionSaga, backendClient.getSession);
}
// check if the current session is still valid
const checkSessionResponse: SagaCallReturnType<typeof checkSession> =
yield* call(
checkSession,
backendClient.getSession,
formatRequestedTokenString()
);
if (checkSessionResponse === 401) {
// This is the first API call we make to the backend, it may happen that
// when we're using the previous session token, that session has expired
// so we need to reset the session token and restart from scratch.
const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector);
if (!isFastLoginEnabled) {
yield* put(sessionExpired());
} else {
yield* put(
refreshSessionToken.request({
withUserInteraction: false,
showIdentificationModalAtStartup: true,
showLoader: false
})
);
}
return;
}
// Now we fork the tasks that will handle the async requests coming from the
// UI of the application.
// Note that the following sagas will be automatically cancelled each time
// this parent saga gets restarted.
yield* fork(
watchUserDataProcessingSaga,
backendClient.getUserDataProcessingRequest,
backendClient.postUserDataProcessingRequest,
backendClient.deleteUserDataProcessingRequest
);
// Start watching for Services actions
yield* fork(watchServicesSaga, backendClient, sessionToken);
// Start watching for Messages actions
yield* fork(watchMessagesSaga, backendClient, sessionToken);
// start watching for FIMS actions
yield* fork(watchFimsSaga);
// watch FCI saga
yield* fork(watchFciSaga, sessionToken, keyInfo);
// whether we asked the user to login again
const isSessionRefreshed = previousSessionToken !== sessionToken;
// Let's see if have to load the session info, either because
// we don't have one for the current session or because we
// just refreshed the session.
// FIXME: since it looks like we load the session info every
// time we get a session token, think about merging the
// two steps.
// eslint-disable-next-line functional/no-let
let maybeSessionInformation: ReturnType<typeof sessionInfoSelector> =
yield* select(sessionInfoSelector);
// In the check below we had also isSessionRefreshed, but it is not needed
// since the actual checkSession made above is enough to ensure that the
// session tokens are retrieved correctly.
// Only in the scenario when we get here and session tokens are not available,
// we have to load the session information from the backend.
// In a future refactoring where the checkSession won't get the session tokens
// anymore, we will need to rethink about this check.
if (
O.isNone(maybeSessionInformation) ||
(O.isSome(maybeSessionInformation) &&
(maybeSessionInformation.value.bpdToken === undefined ||
maybeSessionInformation.value.walletToken === undefined))
) {
// let's try to load the session information from the backend.
maybeSessionInformation = yield* call(
loadSessionInformationSaga,
backendClient.getSession
);
if (
O.isNone(maybeSessionInformation) ||
(O.isSome(maybeSessionInformation) &&
(maybeSessionInformation.value.bpdToken === undefined ||
maybeSessionInformation.value.walletToken === undefined))
) {
yield* call(handleApplicationStartupTransientError, "GET_SESSION_DOWN");
return;
}
}
const publicKey = yield* select(lollipopPublicKeySelector);
// #LOLLIPOP_CHECK_BLOCK2_START
const isAssertionRefValid = yield* call(
checkLollipopSessionAssertionAndInvalidateIfNeeded,
publicKey,
maybeSessionInformation
);
if (!isAssertionRefValid) {
return;
}
// #LOLLIPOP_CHECK_BLOCK2_END
// Start watching for profile update requests as the checkProfileEnabledSaga
// may need to update the profile.
yield* fork(
watchProfileUpsertRequestsSaga,
backendClient.createOrUpdateProfile
);
// Start watching when profile is successfully loaded
yield* fork(watchProfile, backendClient.startEmailValidationProcess);
// If we are here the user is logged in and the session info is
// loaded and valid
// Load the profile info
const maybeUserProfile: SagaCallReturnType<typeof loadProfile> = yield* call(
loadProfile,
backendClient.getProfile
);
if (O.isNone(maybeUserProfile)) {
yield* call(handleApplicationStartupTransientError, "GET_PROFILE_DOWN");
return;
}
yield* put(startupTransientError(startupTransientErrorInitialState));
// eslint-disable-next-line functional/no-let
let userProfile = maybeUserProfile.value;
// If user logged in with different credentials, but this device still has
// user data loaded, then delete data keeping current session (user already
// logged in)
if (
pot.isSome(lastLoggedInProfileState) &&
lastLoggedInProfileState.value.fiscal_code !== userProfile.fiscal_code
) {
// Delete all data while keeping current session:
// Delete the current unlock code from the Keychain
yield* call(deletePin);
// Delete all onboarding data
yield* put(clearOnboarding());
yield* put(clearCache());
}
// Retrieve the configured unlock code from the keychain
const maybeStoredPin: SagaCallReturnType<typeof getPin> = yield* call(getPin);
// Start watching for requests of refresh the profile
yield* fork(watchProfileRefreshRequestsSaga, backendClient.getProfile);
// Start watching for requests about session and support token
yield* fork(
watchCheckSessionSaga,
backendClient.getSession,
formatRequestedTokenString()
);
// Start watching for requests of abort the onboarding
yield* fork(watchGetZendeskTokenSaga, backendClient.getSession);
const watchAbortOnboardingSagaTask = yield* fork(watchAbortOnboardingSaga);
yield* put(startupLoadSuccess(StartupStatusEnum.ONBOARDING));
if (!handleSessionExpiration) {
yield* call(waitForMainNavigator);
}
// yield* delay(0 as Millisecond);
const hasPreviousSessionAndPin =
previousSessionToken && O.isSome(maybeStoredPin);
if (hasPreviousSessionAndPin && showIdentificationModal) {
// we ask the user to identify using the unlock code.
// FIXME: This is an unsafe cast caused by a wrongly described type.
const identificationResult: SagaCallReturnType<
typeof startAndReturnIdentificationResult
> = yield* call(startAndReturnIdentificationResult, maybeStoredPin.value);
if (identificationResult === IdentificationResult.pinreset) {
// If we are here the user had chosen to reset the unlock code
yield* put(startApplicationInitialization());
return;
}
if (!handleSessionExpiration) {
yield* call(setLanguageFromProfileIfExists);
}
const isFastLoginEnabled = yield* select(isFastLoginEnabledSelector);
if (isFastLoginEnabled) {
// At application startup, the state of the refresh token is "idle".
// If we got a 401 in the above getSession we start a token refresh.
// If we succeed, we can continue with the application startup and
// we could skip this step.
const lastTokenRefreshState = yield* select(tokenRefreshSelector);
if (lastTokenRefreshState.kind !== "success") {
yield* put(
refreshSessionToken.request({
withUserInteraction: false,
showIdentificationModalAtStartup: false,
showLoader: true
})
);
return;
}
}
}
// Ask to accept ToS if there is a new available version
yield* call(checkAcceptedTosSaga, userProfile);
if (!handleSessionExpiration) {
yield* call(setLanguageFromProfileIfExists);
}
// check if the user expressed preference about mixpanel, if not ask for it
yield* call(askMixpanelOptIn);
// track if the Android device has StrongBox
yield* call(handleIsKeyStrongboxBacked, keyInfo.keyTag);
yield* call(checkConfiguredPinSaga);
yield* call(checkAcknowledgedFingerprintSaga);
yield* fork(watchEmailValidationSaga);
if (!hasPreviousSessionAndPin || userProfile.email === undefined) {
yield* call(checkAcknowledgedEmailSaga, userProfile);
}
userProfile = (yield* call(checkEmailSaga)) ?? userProfile;
// Check for both profile notifications permissions (anonymous
// content && reminder) and system notifications permissions.
yield* call(profileAndSystemNotificationsPermissions, userProfile);
const isFirstOnboarding = isProfileFirstOnBoarding(userProfile);
yield* call(askServicesPreferencesModeOptin, isFirstOnboarding);
if (isFirstOnboarding) {
// Show the thank-you screen for the onboarding
yield* call(completeOnboardingSaga);
}
// Stop the watchAbortOnboardingSaga
yield* cancel(watchAbortOnboardingSagaTask);
// Fork the saga that uploads the push notification token to the backend.
// At this moment, the push notification token may not be available yet but
// the saga handles it internally. Make sure to fork it and not call it using
// a blocking call, since the saga will just hang, waiting for the token
yield* fork(
pushNotificationTokenUpload,
backendClient.createOrUpdateInstallation
);
// This saga is called before the startup status is set to authenticated to avoid flashing
// the home screen when the user is taken to the alert screen in case of identities that don't match.
yield* call(checkItWalletIdentitySaga);
yield* put(startupLoadSuccess(StartupStatusEnum.AUTHENTICATED));
//
// User is autenticated, session token is valid
//
// Start wathing new wallet sagas
yield* fork(watchNewWalletSaga);
// Here we can be sure that the session information is loaded and valid
const bpdToken = maybeSessionInformation.value.bpdToken as string;
if (cdcEnabled) {
// Start watching for cdc actions
yield* fork(watchBonusCdcSaga, bpdToken);
}
// Start watching for cgn actions
yield* fork(watchBonusCgnSaga, sessionToken);
if (euCovidCertificateEnabled) {
// Start watching for EU Covid Certificate actions
yield* fork(watchEUCovidCertificateSaga, sessionToken);
}
const pnEnabled: ReturnType<typeof isPnEnabledSelector> = yield* select(
isPnEnabledSelector
);
if (pnEnabled) {
// Start watching for PN actions
yield* fork(watchPnSaga, sessionToken);
}
const idPayTestEnabled: ReturnType<typeof isIdPayTestEnabledSelector> =
yield* select(isIdPayTestEnabledSelector);
if (idPayTestEnabled) {
// Start watching for IDPay actions
yield* fork(watchIDPaySaga, bpdToken);
}
// Start watching for trial system saga
yield* fork(watchTrialSystemSaga, sessionToken);
// Start watching for itw saga
yield* fork(watchItwSaga);
// Here we can be sure that the session information is loaded and valid
const walletToken = maybeSessionInformation.value.walletToken as string;
// Start watching for Wallet V3 actions
yield* fork(watchPaymentsSaga, walletToken);
const isPagoPATestEnabled: ReturnType<typeof isPagoPATestEnabledSelector> =
yield* select(isPagoPATestEnabledSelector);
yield* fork(
watchWalletSaga,
sessionToken,
walletToken,
isPagoPATestEnabled ? pagoPaApiUrlPrefixTest : pagoPaApiUrlPrefix
);
// Check that profile is up to date (e.g. inbox enabled)
yield* call(checkProfileEnabledSaga, userProfile);
if (isSessionRefreshed) {
// Only if the user are logging in check the account removal status and,
// if is PENDING show an alert to notify him.
const checkUserDeletePendingTask: any = yield* takeLatest(
loadUserDataProcessing.success,
function* (
loadUserDataProcessingSuccess: ActionType<
typeof loadUserDataProcessing.success
>
) {
const maybeDeletePending = pipe(
loadUserDataProcessingSuccess.payload.value,
O.fromNullable,
O.filter(
uc =>
uc.choice === UserDataProcessingChoiceEnum.DELETE &&
uc.status === UserDataProcessingStatusEnum.PENDING
)
);
type leftOrRight = "left" | "right";
const alertChoiceChannel = channel<leftOrRight>();
const isSettingsVisibleAndHideProfile = yield* select(
isSettingsVisibleAndHideProfileSelector
);
if (O.isSome(maybeDeletePending)) {
Alert.alert(
I18n.t("startup.userDeletePendingAlert.title"),
isSettingsVisibleAndHideProfile
? I18n.t("startup.userDeletePendingAlert.message")
: I18n.t("startup.userDeletePendingAlert.messageLegacy"),
[
{
text: isSettingsVisibleAndHideProfile
? I18n.t("startup.userDeletePendingAlert.cta_1")
: I18n.t("startup.userDeletePendingAlert.cta_1_legacy"),
style: "cancel",
onPress: () => {
alertChoiceChannel.put("left");
}
},
{
text: I18n.t("startup.userDeletePendingAlert.cta_2"),
style: "default",
onPress: () => {
alertChoiceChannel.put("right");
}
}
],
{ cancelable: false }
);
const action: leftOrRight = yield* take(alertChoiceChannel);
if (action === "left") {
yield* call(navigateToPrivacyScreen);
}
yield* cancel(checkUserDeletePendingTask);
}
}
);
yield* put(
loadUserDataProcessing.request(UserDataProcessingChoiceEnum.DELETE)
);
}
// Watch for checking the user email notifications preferences
yield* fork(watchEmailNotificationPreferencesSaga);
// Check if we have a pending notification message
yield* call(handlePendingMessageStateIfAllowed, true);
// This tells the security advice bottomsheet that it can be shown
yield* put(setSecurityAdviceReadyToShow(true));
yield* put(
applicationInitialized({
actionsToWaitFor: [walletPaymentHandlersInitialized]
})
);
}
/**
* Wait until the {@link NavigationService} is initialized.
* The NavigationService is initialized when is called {@link RootContainer} componentDidMount and the ref is set with setTopLevelNavigator
*/
function* waitForNavigatorServiceInitialization() {
// eslint-disable-next-line functional/no-let
let isNavigatorReady: ReturnType<
typeof NavigationService.getIsNavigationReady
> = yield* call(NavigationService.getIsNavigationReady);
// eslint-disable-next-line functional/no-let
let timeoutLogged = false;
const startTime = performance.now();
// before continuing we must wait for the navigatorService to be ready
while (!isNavigatorReady) {
const elapsedTime = performance.now() - startTime;
if (!timeoutLogged && elapsedTime >= warningWaitNavigatorTime) {
timeoutLogged = true;
yield* call(mixpanelTrack, "NAVIGATION_SERVICE_INITIALIZATION_TIMEOUT");
}
yield* delay(navigatorPollingTime);
isNavigatorReady = yield* call(NavigationService.getIsNavigationReady);
}
const initTime = performance.now() - startTime;
yield* call(mixpanelTrack, "NAVIGATION_SERVICE_INITIALIZATION_COMPLETED", {
elapsedTime: initTime
});
}
function* waitForMainNavigator() {
// eslint-disable-next-line functional/no-let
let isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady);
// eslint-disable-next-line functional/no-let
let timeoutLogged = false;
const startTime = performance.now();
// before continuing we must wait for the main navigator tack to be ready
while (!isMainNavReady) {
const elapsedTime = performance.now() - startTime;
if (!timeoutLogged && elapsedTime >= warningWaitNavigatorTime) {
timeoutLogged = true;
yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_TIMEOUT");
}
yield* delay(navigatorPollingTime);
isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady);
}
const initTime = performance.now() - startTime;
yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_OK", {
elapsedTime: initTime
});
}
export function* startupSaga(): IterableIterator<ReduxSagaEffect> {
// Wait until the IngressScreen gets mounted
yield* takeLatest(
getType(startApplicationInitialization),
initializeApplicationSaga
);
}
export const testWaitForNavigatorServiceInitialization = isTestEnv
? waitForNavigatorServiceInitialization
: undefined;