diff --git a/locales/en/index.yml b/locales/en/index.yml index 43350d48fbb..e0e59a0642b 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -1013,6 +1013,11 @@ wallet: title: Vuoi pagare in app? content: Scopri quali sono i metodi che puoi aggiungere al tuo portafoglio. cta: Scopri di più + error: + title: Si è verificato un errore + subtitle: Riprova o contatta l'assistenza + primaryButton: Chiudi + secondaryButton: Riprova wallet: Wallet refreshWallet: Refresh the Wallet favourite: diff --git a/locales/it/index.yml b/locales/it/index.yml index 87e81440b7b..f5a8d5b56b3 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -1013,6 +1013,11 @@ wallet: title: Vuoi pagare in app? content: Scopri quali sono i metodi che puoi aggiungere al tuo portafoglio. cta: Scopri di più + error: + title: Si è verificato un errore + subtitle: Riprova o contatta l'assistenza + primaryButton: Chiudi + secondaryButton: Riprova wallet: Portafoglio refreshWallet: Aggiorna il Portafoglio favourite: diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx index 2eec49f09b4..d371306e44b 100644 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx @@ -1,12 +1,11 @@ import * as React from "react"; -import { RouteProp, useRoute } from "@react-navigation/native"; +import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; import { useDispatch } from "react-redux"; import { IOLogoPaymentExtType } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; -import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; import { PaymentCardBig } from "../../../../components/ui/cards/payment/PaymentCardBig"; import { useIOSelector } from "../../../../store/hooks"; import { idPayAreInitiativesFromInstrumentLoadingSelector } from "../../../idpay/wallet/store/reducers"; @@ -22,6 +21,12 @@ import { import { walletDetailsGetInstrument } from "../store/actions"; import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; import { getDateFromExpiryDate } from "../../../../utils/dates"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import I18n from "../../../../i18n"; +import { + AppParamsList, + IOStackNavigationProp +} from "../../../../navigation/params/AppParamsList"; export type WalletDetailsScreenNavigationParams = Readonly<{ walletId: string; @@ -71,6 +76,7 @@ const generateCardHeaderTitle = (details?: UIWalletInfoDetails) => { */ const WalletDetailsScreen = () => { const route = useRoute(); + const navigation = useNavigation>(); const dispatch = useDispatch(); const { walletId } = route.params; const walletDetails = useIOSelector(walletDetailsInstrumentSelector); @@ -82,6 +88,30 @@ const WalletDetailsScreen = () => { idPayAreInitiativesFromInstrumentLoadingSelector ); + const WalletDetailsGenericFailure = () => ( + navigation.pop() + }} + secondaryAction={{ + label: I18n.t("wallet.methodDetails.error.secondaryButton"), + accessibilityLabel: I18n.t( + "wallet.methodDetails.error.secondaryButton" + ), + onPress: handleOnRetry + }} + /> + ); + + const handleOnRetry = () => { + dispatch(walletDetailsGetInstrument.request({ walletId })); + }; + React.useEffect(() => { dispatch(walletDetailsGetInstrument.request({ walletId })); }, [walletId, dispatch]); @@ -122,7 +152,7 @@ const WalletDetailsScreen = () => { ); } else if (isErrorWalletDetails) { - return ; + return ; } return null; }; diff --git a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx index ae321c1e63e..6654b6bb063 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx @@ -17,7 +17,10 @@ import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { WalletPaymentFeebackBanner } from "../components/WalletPaymentFeedbackBanner"; import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; import { WalletPaymentParamsList } from "../navigation/params"; -import { walletPaymentDetailsSelector } from "../store/selectors"; +import { + walletPaymentDetailsSelector, + walletPaymentStartRouteSelector +} from "../store/selectors"; import { WalletPaymentOutcome, WalletPaymentOutcomeEnum @@ -38,6 +41,7 @@ const WalletPaymentOutcomeScreen = () => { const navigation = useNavigation>(); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); + const paymentStartRoute = useIOSelector(walletPaymentStartRouteSelector); const supportModal = usePaymentFailureSupportModal({ outcome @@ -55,6 +59,12 @@ const WalletPaymentOutcomeScreen = () => { }; const handleClose = () => { + if (paymentStartRoute) { + navigation.navigate(paymentStartRoute.routeName, { + screen: paymentStartRoute.routeKey + }); + return; + } navigation.popToTop(); navigation.pop(); }; diff --git a/ts/features/walletV3/payment/store/actions/orchestration.ts b/ts/features/walletV3/payment/store/actions/orchestration.ts index c306089e00a..cba31c50fa9 100644 --- a/ts/features/walletV3/payment/store/actions/orchestration.ts +++ b/ts/features/walletV3/payment/store/actions/orchestration.ts @@ -2,6 +2,10 @@ import { ActionType, createStandardAction } from "typesafe-actions"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; +/** + * Action to initialize the state of a payment, optionally you can specify the route to go back to + * after the payment is completed or cancelled (default is the popToTop route) + */ export const walletPaymentInitState = createStandardAction( "WALLET_PAYMENT_INIT_STATE" )(); diff --git a/ts/features/walletV3/payment/store/reducers/index.ts b/ts/features/walletV3/payment/store/reducers/index.ts index d96dd69e5c9..036bb117b78 100644 --- a/ts/features/walletV3/payment/store/reducers/index.ts +++ b/ts/features/walletV3/payment/store/reducers/index.ts @@ -1,7 +1,10 @@ import _ from "lodash"; import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; +import { NavigatorScreenParams } from "@react-navigation/native"; import { getType } from "typesafe-actions"; +import { pipe } from "fp-ts/lib/function"; +import { sequenceS } from "fp-ts/lib/Apply"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; import { NewTransactionResponse } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; @@ -27,6 +30,8 @@ import { import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; import { WalletPaymentFailure } from "../../types/failure"; import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import NavigationService from "../../../../../navigation/NavigationService"; +import { AppParamsList } from "../../../../../navigation/params/AppParamsList"; export type WalletPaymentState = { rptId?: RptId; @@ -44,6 +49,10 @@ export type WalletPaymentState = { NetworkError | WalletPaymentFailure >; authorizationUrl: pot.Pot; + startRoute?: { + routeName: keyof AppParamsList; + routeKey: keyof NavigatorScreenParams["screen"]; + }; }; const INITIAL_STATE: WalletPaymentState = { @@ -64,7 +73,21 @@ const reducer = ( ): WalletPaymentState => { switch (action.type) { case getType(walletPaymentInitState): - return INITIAL_STATE; + const startRoute = pipe( + sequenceS(O.Monad)({ + routeName: O.fromNullable( + NavigationService.getCurrentRouteName() as keyof AppParamsList + ), + routeKey: O.fromNullable( + NavigationService.getCurrentRouteKey() as keyof NavigatorScreenParams["screen"] + ) + }), + O.toUndefined + ); + return { + ...INITIAL_STATE, + startRoute + }; // Payment verification and details case getType(walletPaymentGetDetails.request): diff --git a/ts/features/walletV3/payment/store/selectors/index.ts b/ts/features/walletV3/payment/store/selectors/index.ts index 20afcaaf1b8..e122cb38c2f 100644 --- a/ts/features/walletV3/payment/store/selectors/index.ts +++ b/ts/features/walletV3/payment/store/selectors/index.ts @@ -76,3 +76,8 @@ export const walletPaymentAuthorizationUrlSelector = createSelector( selectWalletPayment, state => state.authorizationUrl ); + +export const walletPaymentStartRouteSelector = createSelector( + selectWalletPayment, + state => state.startRoute +); diff --git a/ts/navigation/AppStackNavigator.tsx b/ts/navigation/AppStackNavigator.tsx index b471a8cd648..35c0e5be766 100644 --- a/ts/navigation/AppStackNavigator.tsx +++ b/ts/navigation/AppStackNavigator.tsx @@ -1,5 +1,9 @@ /* eslint-disable functional/immutable-data */ -import { LinkingOptions, NavigationContainer } from "@react-navigation/native"; +import { + LinkingOptions, + NavigationContainer, + NavigationContainerProps +} from "@react-navigation/native"; import * as React from "react"; import { useRef } from "react"; import { View } from "react-native"; @@ -38,10 +42,22 @@ import { useStoredExperimentalDesign } from "../common/context/DSExperimentalCon import { IONavigationLightTheme } from "../theme/navigations"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import AuthenticatedStackNavigator from "./AuthenticatedStackNavigator"; -import NavigationService, { navigationRef } from "./NavigationService"; +import NavigationService, { + navigationRef, + setMainNavigatorReady +} from "./NavigationService"; import NotAuthenticatedStackNavigator from "./NotAuthenticatedStackNavigator"; import ROUTES from "./routes"; +type OnStateChangeStateType = Parameters< + NonNullable +>[0]; +const isMainNavigatorReady = (state: OnStateChangeStateType) => + state && + state.routes && + state.routes.length > 0 && + state.routes[0].name === ROUTES.MAIN; + export const AppStackNavigator = (): React.ReactElement => { // This hook is used since we are in a child of the Context Provider // to setup the experimental design system value from AsyncStorage @@ -153,7 +169,10 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { NavigationService.setNavigationReady(); routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name; }} - onStateChange={async () => { + onStateChange={async state => { + if (isMainNavigatorReady(state)) { + setMainNavigatorReady(); + } const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.current?.getCurrentRoute()?.name; if (currentRouteName !== undefined) { diff --git a/ts/navigation/NavigationService.ts b/ts/navigation/NavigationService.ts index 31b2bc4d218..3679e48dd66 100644 --- a/ts/navigation/NavigationService.ts +++ b/ts/navigation/NavigationService.ts @@ -10,6 +10,13 @@ export const navigationRef = React.createRef(); // eslint-disable-next-line functional/no-let let isNavigationReady: boolean = false; +// eslint-disable-next-line functional/no-let +let isMainNavigatorReady = false; + +export const setMainNavigatorReady = () => { + isMainNavigatorReady = true; +}; + export const setNavigationReady = () => { // eslint-disable-next-line functional/immutable-data isNavigationReady = true; @@ -44,6 +51,8 @@ const dispatchNavigationAction = (action: NavigationAction) => { navigationRef.current?.dispatch(action); }; +const getIsMainNavigatorReady = () => isMainNavigatorReady; + const getCurrentRouteName = (): string | undefined => navigationRef.current?.getCurrentRoute()?.name; @@ -66,5 +75,6 @@ export default { getCurrentRoute, getCurrentState, getIsNavigationReady, - setNavigationReady + setNavigationReady, + getIsMainNavigatorReady }; diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 652e9a5e3c6..e8d0594bb0d 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -426,13 +426,11 @@ export function* initializeApplicationSaga( const watchAbortOnboardingSagaTask = yield* fork(watchAbortOnboardingSaga); yield* put(startupLoadSuccess(StartupStatusEnum.ONBOARDING)); - // FIXME IOPID-1298: find any better way to handle this - // We need this workaround to let the inner AppStackNavigator stack be ready, - // before continuing with any other navigation action to avoid: - // Error: The 'navigation' object hasn't been initialized yet... - // Here the navigationRef is ready, but because we changed the navigation inner stack - // based on StartupStatusEnum value, we need to wait for the new stack to be ready. - yield* delay(0 as Millisecond); + if (!handleSessionExpiration) { + yield* call(waitForMainNavigator); + } + + // yield* delay(0 as Millisecond); const hasPreviousSessionAndPin = previousSessionToken && O.isSome(maybeStoredPin); if (hasPreviousSessionAndPin && showIdentificationModal) { @@ -517,9 +515,6 @@ export function* initializeApplicationSaga( yield* call(updateInstallationSaga, backendClient.createOrUpdateInstallation); yield* put(startupLoadSuccess(StartupStatusEnum.AUTHENTICATED)); - // FIXME IOPID-1298: find any better way to handle this - // As above for StartupStatusEnum.ONBOARDING - yield* delay(0 as Millisecond); // // User is autenticated, session token is valid // @@ -695,6 +690,33 @@ function* waitForNavigatorServiceInitialization() { }); } +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 + }); +} + /** * Remove all the local notifications related to authentication with spid. *