teamdigitale/italia-app

View on GitHub
ts/features/bonus/cgn/screens/merchants/CgnMerchantsListScreen.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import { Millisecond } from "@pagopa/ts-commons/lib/units";
import { debounce } from "lodash";
import * as React from "react";
import { useCallback, useMemo } from "react";
import { FlatList, Keyboard, RefreshControl, SafeAreaView } from "react-native";
import { connect } from "react-redux";
import {
  ContentWrapper,
  Divider,
  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 { IOStyles } from "../../../../../components/core/variables/IOStyles";
import I18n from "../../../../../i18n";
import { Dispatch } from "../../../../../store/actions/types";
import { GlobalState } from "../../../../../store/reducers/types";
import { LoadingErrorComponent } from "../../../../../components/LoadingErrorComponent";
import {
  getValueOrElse,
  isError,
  isLoading,
  isReady
} from "../../../../../common/model/RemoteValue";
import { CgnMerchantListViewRenderItem } from "../../components/merchants/CgnMerchantsListView";
import { navigateToCgnMerchantDetail } from "../../navigation/actions";
import {
  cgnOfflineMerchants,
  cgnOnlineMerchants
} from "../../store/actions/merchants";
import {
  cgnOfflineMerchantsSelector,
  cgnOnlineMerchantsSelector
} from "../../store/reducers/merchants";
import { mixAndSortMerchants } from "../../utils/merchants";

type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps>;

const DEBOUNCE_SEARCH: Millisecond = 300 as Millisecond;

export type MerchantsAll = OfflineMerchant | OnlineMerchant;
/**
 * Screen that renders the list of the merchants which have an active discount for CGN
 * @param props
 * @constructor
 */
const CgnMerchantsListScreen: React.FunctionComponent<Props> = (
  props: Props
) => {
  const [searchValue, setSearchValue] = React.useState("");
  const [merchantList, setMerchantsList] = React.useState<
    ReadonlyArray<MerchantsAll>
  >([]);

  const { navigateToMerchantDetail } = props;

  // Mixes online and offline merchants to render on the same list
  // merchants are sorted by name
  const merchantsAll = useMemo(
    () =>
      mixAndSortMerchants(
        getValueOrElse(props.onlineMerchants, []),
        getValueOrElse(props.offlineMerchants, [])
      ),
    [props.onlineMerchants, props.offlineMerchants]
  );

  const performSearch = (
    text: string,
    merchantList: ReadonlyArray<MerchantsAll>
  ) => {
    // if search text is empty, restore the whole list
    if (text.length === 0) {
      setMerchantsList(merchantList);
      return;
    }
    const resultList = merchantList.filter(
      m => m.name.toLowerCase().indexOf(text.toLowerCase()) > -1
    );
    setMerchantsList(resultList);
  };

  const debounceRef = React.useRef(debounce(performSearch, DEBOUNCE_SEARCH));

  React.useEffect(() => {
    debounceRef.current(searchValue, merchantsAll);
  }, [
    searchValue,
    props.onlineMerchants,
    props.offlineMerchants,
    merchantsAll
  ]);

  const { requestOfflineMerchants, requestOnlineMerchants } = props;

  const initLoadingLists = useCallback(() => {
    requestOfflineMerchants();
    requestOnlineMerchants();
  }, [requestOfflineMerchants, requestOnlineMerchants]);

  useFocusEffect(initLoadingLists);

  const onItemPress = React.useCallback(
    (id: Merchant["id"]) => {
      navigateToMerchantDetail(id);
      Keyboard.dismiss();
    },
    [navigateToMerchantDetail]
  );

  const renderItem = React.useMemo(
    () => CgnMerchantListViewRenderItem({ onItemPress }),
    [onItemPress]
  );

  return (
    <SafeAreaView style={IOStyles.flex}>
      {!(isError(props.onlineMerchants) || isError(props.offlineMerchants)) && (
        <ContentWrapper>
          <ListItemHeader
            label={I18n.t("bonus.cgn.merchantsList.merchantsAll")}
          />
          <TextInput
            accessibilityLabel={I18n.t("global.buttons.search")}
            icon="search"
            value={searchValue}
            onChangeText={setSearchValue}
            placeholder={I18n.t("global.buttons.search")}
            autoFocus={false}
          />
          <VSpacer />
        </ContentWrapper>
      )}
      {isReady(props.onlineMerchants) || isReady(props.offlineMerchants) ? (
        <FlatList
          data={merchantList}
          keyExtractor={item => item.id}
          renderItem={renderItem}
          ItemSeparatorComponent={() => <Divider />}
          refreshControl={
            <RefreshControl
              refreshing={
                isLoading(props.onlineMerchants) ||
                isLoading(props.offlineMerchants)
              }
              onRefresh={initLoadingLists}
            />
          }
        />
      ) : (
        <LoadingErrorComponent
          isLoading={
            isLoading(props.offlineMerchants) ||
            isLoading(props.onlineMerchants)
          }
          loadingCaption={I18n.t("global.remoteStates.loading")}
          onRetry={initLoadingLists}
        />
      )}
    </SafeAreaView>
  );
};

const mapStateToProps = (state: GlobalState) => ({
  onlineMerchants: cgnOnlineMerchantsSelector(state),
  offlineMerchants: cgnOfflineMerchantsSelector(state)
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  requestOnlineMerchants: () => dispatch(cgnOnlineMerchants.request({})),
  requestOfflineMerchants: () => dispatch(cgnOfflineMerchants.request({})),
  navigateToMerchantDetail: (id: Merchant["id"]) =>
    navigateToCgnMerchantDetail({ merchantID: id })
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CgnMerchantsListScreen);