diff --git a/locales/en/index.yml b/locales/en/index.yml index 96b8914764d..65f1448ac6c 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -2525,11 +2525,16 @@ bonus: online: Online places: Places navigationTitle: Benefits - screenTitle: Select the operator and discover the benefits + screenTitle: Find the benefits + merchantsAll: All the operators cta: filter: Filter categoriesList: title: Select a category and discover the benefits + bottomSheet: + cta: How are categories ordered by? + title: How are categories ordered by? + content: To facilitate your search and improve your experience, we have organized the categories according to frequency of use.This information is not tied to your profile, but is based on aggregate data from all users who use Carta Giovani Nazionale and have consented to data processing. filter: title: Filter operators searchTitle: Find by name diff --git a/locales/it/index.yml b/locales/it/index.yml index 2b36879ea1d..dda3a563887 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -2525,11 +2525,16 @@ bonus: online: Online places: Luoghi navigationTitle: Agevolazioni - screenTitle: Seleziona un operatore e scopri agevolazioni e opportunità + screenTitle: Scopri le proposte + merchantsAll: Tutti gli operatori cta: filter: Filtra categoriesList: title: Seleziona una categoria e scopri agevolazioni e opportunità + bottomSheet: + cta: Come vengono ordinate le categorie? + title: Come vengono ordinate le categorie? + content: Per agevolare la ricerca e migliorare la tua esperienza, abbiamo organizzato le categorie in base alla frequenza di utilizzo.Questa informazione non è legata al tuo profilo, ma si basa su dati complessivi di tutti gli utenti che utilizzano Carta Giovani Nazionale e hanno acconsentito al trattamento dei dati. filter: title: Filtra esercenti searchTitle: Cerca per nome diff --git a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx index be91b904616..c3efc832168 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx +++ b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx @@ -1,12 +1,13 @@ import * as React from "react"; import { View, FlatList, ListRenderItemInfo, Platform } from "react-native"; +import { Badge, H6, ListItemNav } from "@pagopa/io-app-design-system"; import { IOStyles } from "../../../../../components/core/variables/IOStyles"; import ItemSeparatorComponent from "../../../../../components/ItemSeparatorComponent"; import { EdgeBorderComponent } from "../../../../../components/screens/EdgeBorderComponent"; import { OfflineMerchant } from "../../../../../../definitions/cgn/merchants/OfflineMerchant"; import { OnlineMerchant } from "../../../../../../definitions/cgn/merchants/OnlineMerchant"; import { Merchant } from "../../../../../../definitions/cgn/merchants/Merchant"; -import CgnMerchantListItem from "./CgnMerchantListItem"; +import I18n from "../../../../../i18n"; type Props = { merchantList: ReadonlyArray; @@ -20,12 +21,29 @@ const CgnMerchantsListView: React.FunctionComponent = (props: Props) => { const renderListItem = ( listItem: ListRenderItemInfo ) => ( - props.onItemPress(listItem.item.id)} - isNew={listItem.item.newDiscounts} - /> + <> + props.onItemPress(listItem.item.id)} + value={ + listItem.item.newDiscounts ? ( + +
+ {listItem.item.name} +
+ + + +
+ ) : ( + listItem.item.name + ) + } + accessibilityLabel={listItem.item.name} + /> + ); return ( diff --git a/ts/features/bonus/cgn/navigation/actions.ts b/ts/features/bonus/cgn/navigation/actions.ts index e37c5e16398..2751751e947 100644 --- a/ts/features/bonus/cgn/navigation/actions.ts +++ b/ts/features/bonus/cgn/navigation/actions.ts @@ -106,17 +106,6 @@ export const navigateToCgnDetails = () => ); // Merchants -/** - * @deprecated Do not use this method when you have access to a navigation prop or useNavigation since it will behave differently, - * and many helper methods specific to screens won't be available. - */ -export const navigateToCgnMerchantsList = () => - NavigationService.dispatchNavigationAction( - CommonActions.navigate(CGN_ROUTES.DETAILS.MAIN, { - screen: CGN_ROUTES.DETAILS.MERCHANTS.LIST - }) - ); - /** * @deprecated Do not use this method when you have access to a navigation prop or useNavigation since it will behave differently, * and many helper methods specific to screens won't be available. diff --git a/ts/features/bonus/cgn/navigation/navigator.tsx b/ts/features/bonus/cgn/navigation/navigator.tsx index 650f18e2ab8..fa1c140f227 100644 --- a/ts/features/bonus/cgn/navigation/navigator.tsx +++ b/ts/features/bonus/cgn/navigation/navigator.tsx @@ -16,7 +16,6 @@ import CgnMerchantDetailScreen from "../screens/merchants/CgnMerchantDetailScree import CgnMerchantLandingWebview from "../screens/merchants/CgnMerchantLandingWebview"; import CgnMerchantsCategoriesSelectionScreen from "../screens/merchants/CgnMerchantsCategoriesSelectionScreen"; import CgnMerchantsListByCategory from "../screens/merchants/CgnMerchantsListByCategory"; -import MerchantsListScreen from "../screens/merchants/CgnMerchantsListScreen"; import CgnMerchantsTabsScreen from "../screens/merchants/CgnMerchantsTabsScreen"; import { AppParamsList } from "../../../../navigation/params/AppParamsList"; import { @@ -93,34 +92,35 @@ const DetailStack = createStackNavigator(); export const CgnDetailsNavigator = () => ( - diff --git a/ts/features/bonus/cgn/navigation/params.ts b/ts/features/bonus/cgn/navigation/params.ts index 4b8eadeee9b..323a2082023 100644 --- a/ts/features/bonus/cgn/navigation/params.ts +++ b/ts/features/bonus/cgn/navigation/params.ts @@ -17,7 +17,6 @@ export type CgnActivationParamsList = { export type CgnDetailsParamsList = { [CGN_ROUTES.DETAILS.DETAILS]: undefined; [CGN_ROUTES.DETAILS.MERCHANTS.CATEGORIES]: undefined; - [CGN_ROUTES.DETAILS.MERCHANTS.LIST]: undefined; [CGN_ROUTES.DETAILS.MERCHANTS .LIST_BY_CATEGORY]: CgnMerchantListByCategoryScreenNavigationParams; [CGN_ROUTES.DETAILS.MERCHANTS.TABS]: undefined; diff --git a/ts/features/bonus/cgn/navigation/routes.ts b/ts/features/bonus/cgn/navigation/routes.ts index eacd995f598..354621d11aa 100644 --- a/ts/features/bonus/cgn/navigation/routes.ts +++ b/ts/features/bonus/cgn/navigation/routes.ts @@ -21,7 +21,6 @@ const CGN_ROUTES = { DETAILS: "CGN_DETAILS", MERCHANTS: { CATEGORIES: "CGN_MERCHANTS_CATEGORIES", - LIST: "CGN_MERCHANTS_LIST", LIST_BY_CATEGORY: "CGN_MERCHANTS_LIST_BY_CATEGORY", TABS: "CGN_MERCHANTS_TABS", DETAIL: "CGN_MERCHANTS_DETAIL", diff --git a/ts/features/bonus/cgn/screens/CgnDetailScreen.tsx b/ts/features/bonus/cgn/screens/CgnDetailScreen.tsx index bccc93729a6..f41112578d0 100644 --- a/ts/features/bonus/cgn/screens/CgnDetailScreen.tsx +++ b/ts/features/bonus/cgn/screens/CgnDetailScreen.tsx @@ -37,10 +37,7 @@ import CgnOwnershipInformation from "../components/detail/CgnOwnershipInformatio import CgnStatusDetail from "../components/detail/CgnStatusDetail"; import CgnUnsubscribe from "../components/detail/CgnUnsubscribe"; import EycaDetailComponent from "../components/detail/eyca/EycaDetailComponent"; -import { - navigateToCgnMerchantsList, - navigateToCgnMerchantsTabs -} from "../navigation/actions"; +import { navigateToCgnMerchantsTabs } from "../navigation/actions"; import { CgnDetailsParamsList } from "../navigation/params"; import CGN_ROUTES from "../navigation/routes"; import { cgnDetails } from "../store/actions/details"; @@ -220,7 +217,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ goBack: () => navigateBack(), loadEycaDetails: () => dispatch(cgnEycaStatus.request()), loadCgnDetails: () => dispatch(cgnDetails.request()), - navigateToMerchantsList: () => navigateToCgnMerchantsList(), navigateToMerchantsTabs: () => navigateToCgnMerchantsTabs() }); diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantCategoriesListScreen.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantCategoriesListScreen.tsx new file mode 100644 index 00000000000..a9c3277baa2 --- /dev/null +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantCategoriesListScreen.tsx @@ -0,0 +1,146 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { + Badge, + Body, + H6, + IOStyles, + IOToast, + ListItemAction, + ListItemNav +} from "@pagopa/io-app-design-system"; +import * as React from "react"; +import { FlatList, RefreshControl, View } from "react-native"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import { useNavigation } from "@react-navigation/native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ProductCategoryWithNewDiscountsCount } from "../../../../../../definitions/cgn/merchants/ProductCategoryWithNewDiscountsCount"; +import { getCategorySpecs } from "../../utils/filters"; +import I18n from "../../../../../i18n"; +import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList"; +import { CgnDetailsParamsList } from "../../navigation/params"; +import { useIODispatch, useIOSelector } from "../../../../../store/hooks"; +import { cgnCategoriesListSelector } from "../../store/reducers/categories"; +import { cgnCategories } from "../../store/actions/categories"; +import CGN_ROUTES from "../../navigation/routes"; +import { useIOBottomSheetAutoresizableModal } from "../../../../../utils/hooks/bottomSheet"; + +export const CgnMerchantCategoriesListScreen = () => { + const insets = useSafeAreaInsets(); + const isFirstRender = React.useRef(true); + const dispatch = useIODispatch(); + const potCategories = useIOSelector(cgnCategoriesListSelector); + const navigation = + useNavigation< + IOStackNavigationProp + >(); + + const { present, bottomSheet } = useIOBottomSheetAutoresizableModal({ + fullScreen: true, + title: I18n.t("bonus.cgn.merchantsList.categoriesList.bottomSheet.title"), + component: ( + + + {I18n.t("bonus.cgn.merchantsList.categoriesList.bottomSheet.content")} + + + ) + }); + + const loadCategories = () => { + dispatch(cgnCategories.request()); + }; + React.useEffect(loadCategories, [dispatch]); + + const isError = React.useMemo( + () => pot.isError(potCategories), + [potCategories] + ); + + React.useEffect(() => { + if (!isFirstRender.current && isError) { + IOToast.error(I18n.t("global.genericError")); + } + // eslint-disable-next-line functional/immutable-data + isFirstRender.current = false; + }, [isError]); + + const renderCategoryElement = ( + category: ProductCategoryWithNewDiscountsCount, + i: number + ) => { + const specs = getCategorySpecs(category.productCategory); + const countAvailable = category.newDiscounts > 0; + return pipe( + specs, + O.fold( + () => null, + s => ( + +
{I18n.t(s.nameKey)}
+ + + ) : ( + I18n.t(s.nameKey) + ) + } + accessibilityLabel={I18n.t(s.nameKey)} + onPress={() => { + navigation.navigate( + CGN_ROUTES.DETAILS.MERCHANTS.LIST_BY_CATEGORY, + { + category: s.type + } + ); + }} + icon={s.icon} + /> + ) + ) + ); + }; + + const categoriesToArray: ReadonlyArray = + React.useMemo( + () => [...(pot.isSome(potCategories) ? potCategories.value : [])], + [potCategories] + ); + + return ( + <> + {bottomSheet} + pc.productCategory} + renderItem={({ item, index }) => renderCategoryElement(item, index)} + refreshControl={ + + } + ListFooterComponent={ + + } + /> + + ); +}; diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx index f3add25c6cd..03a41cb8897 100644 --- a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsCategoriesSelectionScreen.tsx @@ -1,178 +1,126 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useNavigation } from "@react-navigation/native"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import * as React from "react"; -import { useEffect, useMemo, useRef } from "react"; -import { View, FlatList, ListRenderItemInfo, Platform } from "react-native"; +import { View } from "react-native"; import { - getGradientColorValues, + H2, + TabNavigation, + TabItem, VSpacer, - Badge, - IOToast, - Icon + IOIcons } from "@pagopa/io-app-design-system"; -import { ProductCategoryWithNewDiscountsCount } from "../../../../../../definitions/cgn/merchants/ProductCategoryWithNewDiscountsCount"; -import { H1 } from "../../../../../components/core/typography/H1"; +import { + MaterialTopTabBarProps, + createMaterialTopTabNavigator +} from "@react-navigation/material-top-tabs"; import { IOStyles } from "../../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../../components/screens/BaseScreenComponent"; -import { EdgeBorderComponent } from "../../../../../components/screens/EdgeBorderComponent"; import I18n from "../../../../../i18n"; -import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../../../store/hooks"; -import { emptyContextualHelp } from "../../../../../utils/emptyContextualHelp"; -import CgnMerchantCategoryItem from "../../components/merchants/CgnMerchantCategoryItem"; -import { CgnDetailsParamsList } from "../../navigation/params"; -import CGN_ROUTES from "../../navigation/routes"; -import { cgnCategories } from "../../store/actions/categories"; -import { cgnCategoriesListSelector } from "../../store/reducers/categories"; -import { getCategorySpecs } from "../../utils/filters"; +import { useHeaderSecondLevel } from "../../../../../hooks/useHeaderSecondLevel"; +import CgnMerchantsListScreen from "./CgnMerchantsListScreen"; +import { CgnMerchantCategoriesListScreen } from "./CgnMerchantCategoriesListScreen"; -const CgnMerchantsCategoriesSelectionScreen = () => { - const isFirstRender = useRef(true); - const dispatch = useIODispatch(); - const potCategories = useIOSelector(cgnCategoriesListSelector); - const navigation = - useNavigation< - IOStackNavigationProp - >(); +export const CgnMerchantsHomeTabRoutes = { + CGN_CATEGORIES: "CGN_CATEGORIES", + CGN_MERCHANTS_ALL: "CGN_MERCHANTS_ALL" +} as const; - const loadCategories = () => { - dispatch(cgnCategories.request()); - }; +export type CgnMerchantsHomeTabParamsList = { + [CgnMerchantsHomeTabRoutes.CGN_CATEGORIES]: undefined; + [CgnMerchantsHomeTabRoutes.CGN_MERCHANTS_ALL]: undefined; +}; - useEffect(loadCategories, [dispatch]); +const Tab = createMaterialTopTabNavigator(); + +type TabOption = { + title: string; + icon: IOIcons; +}; - const isError = useMemo(() => pot.isError(potCategories), [potCategories]); +const tabOptions: Record = { + [CgnMerchantsHomeTabRoutes.CGN_CATEGORIES]: { + icon: "initiatives", + title: "Per categoria" + }, + [CgnMerchantsHomeTabRoutes.CGN_MERCHANTS_ALL]: { + icon: "merchant", + title: "Per operatore" + } +}; - useEffect(() => { - if (!isFirstRender.current && isError) { - IOToast.error(I18n.t("global.genericError")); - } - // eslint-disable-next-line functional/immutable-data - isFirstRender.current = false; - }, [isError]); +const CgnTabBar = ({ state, navigation }: MaterialTopTabBarProps) => { + const isFocused = React.useCallback( + (i: number) => state.index === i, + [state] + ); - const renderCategoryElement = ( - info: ListRenderItemInfo< - | ProductCategoryWithNewDiscountsCount - | { productCategory: "All"; newDiscounts: number } - > - ) => { - if (info.item.productCategory === "All") { - return ( - navigation.navigate(CGN_ROUTES.DETAILS.MERCHANTS.LIST)} - child={ - - - - - - - } - /> - ); - } - const specs = getCategorySpecs(info.item.productCategory); - const countAvailable = info.item.newDiscounts > 0; - return pipe( - specs, - O.fold( - () => null, - s => { - const categoryIcon = ( - - {countAvailable && ( - - - - - - )} - - - ); + return ( + + + {state.routes.map((route, index) => { + const onPress = () => { + const event = navigation.emit({ + type: "tabPress", + target: route.key, + canPreventDefault: true + }); + if (!isFocused(index) && !event.defaultPrevented) { + navigation.navigate(route.name); + } + }; + + const label = + tabOptions[route.name as keyof CgnMerchantsHomeTabParamsList].title; return ( - { - navigation.navigate( - CGN_ROUTES.DETAILS.MERCHANTS.LIST_BY_CATEGORY, - { - category: s.type - } - ); - }} - child={categoryIcon} + ); - } - ) - ); - }; - - const allNews = pot.isSome(potCategories) - ? potCategories.value.reduce( - (acc, val) => acc + (val.newDiscounts as number), - 0 - ) - : 0; - - const categoriesToArray: ReadonlyArray< - | ProductCategoryWithNewDiscountsCount - | { productCategory: "All"; newDiscounts: number } - > = [ - { productCategory: "All", newDiscounts: allNews }, - ...(pot.isSome(potCategories) ? potCategories.value : []) - ]; + })} + + + + ); +}; +const CgnMerchantsCategoriesSelectionScreen = () => { + useHeaderSecondLevel({ + title: "", + canGoBack: true, + supportRequest: true + }); return ( - - -

{I18n.t("bonus.cgn.merchantsList.categoriesList.title")}

- - item.productCategory} - ListFooterComponent={} - /> + <> + +

+ {I18n.t("bonus.cgn.merchantsList.screenTitle")} +

-
+ + + + + + ); }; diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListScreen.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListScreen.tsx index 37ac4aae985..9165493a88e 100644 --- a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListScreen.tsx +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListScreen.tsx @@ -2,22 +2,26 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; import { debounce } from "lodash"; import * as React from "react"; import { useCallback, useMemo } from "react"; -import { View, Keyboard, SafeAreaView } from "react-native"; +import { Keyboard, SafeAreaView } from "react-native"; import { connect } from "react-redux"; +import { + ContentWrapper, + ListItemHeader, + TextInput, + VSpacer +} from "@pagopa/io-app-design-system"; +import { useFocusEffect } from "@react-navigation/native"; import { Merchant } from "../../../../../../definitions/cgn/merchants/Merchant"; import { OfflineMerchant } from "../../../../../../definitions/cgn/merchants/OfflineMerchant"; import { OnlineMerchant } from "../../../../../../definitions/cgn/merchants/OnlineMerchant"; -import { H1 } from "../../../../../components/core/typography/H1"; import { IOStyles } from "../../../../../components/core/variables/IOStyles"; -import { LabelledItem } from "../../../../../components/LabelledItem"; -import BaseScreenComponent from "../../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../../i18n"; import { Dispatch } from "../../../../../store/actions/types"; import { GlobalState } from "../../../../../store/reducers/types"; -import { emptyContextualHelp } from "../../../../../utils/emptyContextualHelp"; import { LoadingErrorComponent } from "../../../../../components/LoadingErrorComponent"; import { getValueOrElse, + isError, isLoading, isReady } from "../../../../../common/model/RemoteValue"; @@ -96,7 +100,7 @@ const CgnMerchantsListScreen: React.FunctionComponent = ( requestOnlineMerchants(); }, [requestOfflineMerchants, requestOnlineMerchants]); - React.useEffect(initLoadingLists, [initLoadingLists]); + useFocusEffect(initLoadingLists); const onItemPress = (id: Merchant["id"]) => { props.navigateToMerchantDetail(id); @@ -104,51 +108,44 @@ const CgnMerchantsListScreen: React.FunctionComponent = ( }; return ( - - - {isReady(props.onlineMerchants) || isReady(props.offlineMerchants) ? ( - <> - -

{I18n.t("bonus.cgn.merchantsList.screenTitle")}

- - - -
- - - ) : ( - + {!(isError(props.onlineMerchants) || isError(props.offlineMerchants)) && ( + + + - )} -
-
+ + + )} + {isReady(props.onlineMerchants) || isReady(props.offlineMerchants) ? ( + + ) : ( + + )} + ); };