teamdigitale/italia-app

View on GitHub
ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { H2, VSpacer } from "@pagopa/io-app-design-system";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { useFocusEffect } from "@react-navigation/native";
import { sequenceT } from "fp-ts/lib/Apply";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import React from "react";
import _ from "lodash";
import I18n from "../../../../i18n";
import { useIONavigation } from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import {
  CheckoutPaymentMethodsList,
  CheckoutPaymentMethodsListSkeleton
} from "../components/CheckoutPaymentMethodsList";
import { useOnTransactionActivationEffect } from "../hooks/useOnTransactionActivationEffect";
import { PaymentsCheckoutRoutes } from "../navigation/routes";
import {
  paymentsCalculatePaymentFeesAction,
  paymentsCreateTransactionAction,
  paymentsGetPaymentMethodsAction,
  paymentsGetRecentPaymentMethodUsedAction
} from "../store/actions/networking";
import {
  walletPaymentAmountSelector,
  walletPaymentDetailsSelector
} from "../store/selectors";
import {
  walletPaymentAllMethodsSelector,
  walletPaymentEnabledUserWalletsSelector,
  walletPaymentSelectedPaymentMethodIdOptionSelector,
  walletPaymentSelectedWalletIdOptionSelector
} from "../store/selectors/paymentMethods";
import { walletPaymentPspListSelector } from "../store/selectors/psps";
import {
  walletPaymentIsTransactionActivatedSelector,
  walletPaymentTransactionSelector
} from "../store/selectors/transaction";
import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum";
import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails";
import * as analytics from "../analytics";
import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
import { paymentAnalyticsDataSelector } from "../../history/store/selectors";
import { PaymentAnalyticsSelectedMethodFlag } from "../../common/types/PaymentAnalytics";
import { IOScrollView } from "../../../../components/ui/IOScrollView";

const WalletPaymentPickMethodScreen = () => {
  const dispatch = useIODispatch();
  const navigation = useIONavigation();

  const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector);
  const paymentAmountPot = useIOSelector(walletPaymentAmountSelector);
  const paymentMethodsPot = useIOSelector(walletPaymentAllMethodsSelector);
  const userWalletsPots = useIOSelector(
    walletPaymentEnabledUserWalletsSelector
  );
  const transactionPot = useIOSelector(walletPaymentTransactionSelector);
  const isTransactionAlreadyActivated = useIOSelector(
    walletPaymentIsTransactionActivatedSelector
  );
  const pspListPot = useIOSelector(walletPaymentPspListSelector);
  const paymentAnalyticsData = useIOSelector(paymentAnalyticsDataSelector);

  const selectedWalletIdOption = useIOSelector(
    walletPaymentSelectedWalletIdOptionSelector
  );
  const selectedPaymentMethodIdOption = useIOSelector(
    walletPaymentSelectedPaymentMethodIdOptionSelector
  );
  const [waitingTransactionActivation, setWaitingTransactionActivation] =
    React.useState(false);

  useFocusEffect(
    React.useCallback(() => {
      dispatch(paymentsGetPaymentMethodsAction.request());
    }, [dispatch])
  );

  useOnFirstRender(() => {
    dispatch(paymentsGetRecentPaymentMethodUsedAction.request());
  });

  const calculateFeesForSelectedPaymentMethod = React.useCallback(() => {
    pipe(
      sequenceT(O.Monad)(
        pot.toOption(paymentAmountPot),
        pot.toOption(transactionPot),
        selectedPaymentMethodIdOption
      ),
      O.map(([paymentAmount, transaction, paymentMethodId]) => {
        // We can safely get this data from the first payment object
        // This logic should be revisited once the cart feature will be implemented
        const primaryPayment = transaction.payments[0];

        const transferList = primaryPayment.transferList ?? [];
        const paymentToken = primaryPayment?.paymentToken;
        const primaryTransfer = primaryPayment?.transferList?.[0];
        const isAllCCP = primaryPayment?.isAllCCP;
        const primaryCreditorInstitution = primaryTransfer?.paFiscalCode;

        // In case of guest payment walletId could be undefined
        const walletId = O.toUndefined(selectedWalletIdOption);

        // In case of an onboarded wallet, it could be present the idPsp that needs to be preselected in the calculateFees request
        const idPsp = pipe(
          userWalletsPots,
          pot.toOption,
          O.chainNullableK(wallets =>
            wallets.find(wallet => wallet.walletId === walletId)
          ),
          O.chainNullableK(wallet => wallet.details as UIWalletInfoDetails),
          O.map(details => details.pspId),
          O.getOrElseW(() => undefined)
        );

        dispatch(
          paymentsCalculatePaymentFeesAction.request({
            paymentToken,
            paymentMethodId,
            walletId,
            paymentAmount,
            transferList,
            isAllCCP,
            primaryCreditorInstitution,
            idPsp
          })
        );
      })
    );
    setWaitingTransactionActivation(false);
  }, [
    dispatch,
    paymentAmountPot,
    transactionPot,
    userWalletsPots,
    selectedPaymentMethodIdOption,
    selectedWalletIdOption
  ]);

  // When a new transaction is created it comes with ACTIVATION_REQUESTED status, we can continue the payment flow
  // only when the transaction status becomes ACTIVATED.
  useOnTransactionActivationEffect(calculateFeesForSelectedPaymentMethod);

  const isLoading =
    pot.isLoading(paymentMethodsPot) || pot.isLoading(userWalletsPots);

  const isLoadingTransaction =
    pot.isLoading(transactionPot) ||
    waitingTransactionActivation ||
    pot.isLoading(pspListPot);

  const isError =
    pot.isError(transactionPot) ||
    pot.isError(paymentMethodsPot) ||
    pot.isError(userWalletsPots) ||
    pot.isError(pspListPot);

  useOnFirstRender(
    () => {
      analytics.trackPaymentMethodSelection({
        attempt: paymentAnalyticsData?.attempt,
        organization_name: paymentAnalyticsData?.verifiedData?.paName,
        organization_fiscal_code:
          paymentAnalyticsData?.verifiedData?.paFiscalCode,
        service_name: paymentAnalyticsData?.serviceName,
        amount: paymentAnalyticsData?.formattedAmount,
        saved_payment_method:
          paymentAnalyticsData?.savedPaymentMethods?.length || 0,
        saved_payment_method_unavailable:
          paymentAnalyticsData?.savedPaymentMethodsUnavailable?.length,
        last_used_payment_method: "no", // <- TODO: This should be dynamic when the feature will be implemented
        expiration_date: paymentAnalyticsData?.verifiedData?.dueDate
      });
    },
    () => !isLoading && !!paymentAnalyticsData
  );

  React.useEffect(() => {
    if (isError && !pot.isError(transactionPot)) {
      navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, {
        screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME,
        params: {
          outcome: WalletPaymentOutcomeEnum.GENERIC_ERROR
        }
      });
    } else if (isError && pot.isError(transactionPot)) {
      navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, {
        screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_FAILURE,
        params: {
          error: transactionPot.error
        }
      });
    }
  }, [isError, navigation, transactionPot]);

  const canContinue = O.isSome(selectedPaymentMethodIdOption);

  const getSelectedPaymentMethodFlag = () =>
    pipe(
      sequenceT(O.Monad)(selectedPaymentMethodIdOption, selectedWalletIdOption),
      O.fold(
        () => "none" as PaymentAnalyticsSelectedMethodFlag,
        () => "saved" as PaymentAnalyticsSelectedMethodFlag
      )
    );

  const handleOnCreateTransactionError = () => {
    setWaitingTransactionActivation(false);
  };

  const handleContinue = () => {
    analytics.trackPaymentMethodSelected({
      attempt: paymentAnalyticsData?.attempt,
      organization_name: paymentAnalyticsData?.verifiedData?.paName,
      organization_fiscal_code:
        paymentAnalyticsData?.verifiedData?.paFiscalCode,
      service_name: paymentAnalyticsData?.serviceName,
      amount: paymentAnalyticsData?.formattedAmount,
      expiration_date: paymentAnalyticsData?.verifiedData?.dueDate,
      payment_method_selected: paymentAnalyticsData?.selectedPaymentMethod,
      payment_method_selected_flag: getSelectedPaymentMethodFlag()
    });
    if (isTransactionAlreadyActivated) {
      // If transacion is already activated (for example, when the user returns to this screen to edit the selected
      // method) we can go directly to the next step.
      calculateFeesForSelectedPaymentMethod();
    } else {
      pipe(
        pot.toOption(paymentDetailsPot),
        O.map(paymentDetails => {
          dispatch(
            paymentsCreateTransactionAction.request({
              data: {
                paymentNotices: [
                  { rptId: paymentDetails.rptId, amount: paymentDetails.amount }
                ]
              },
              onError: handleOnCreateTransactionError
            })
          );
          setWaitingTransactionActivation(true);
        })
      );
    }
  };

  return (
    <IOScrollView
      actions={
        canContinue
          ? {
              type: "SingleButton",
              primary: {
                label: I18n.t("global.buttons.continue"),
                accessibilityLabel: I18n.t("global.buttons.continue"),
                onPress: handleContinue,
                disabled: isLoading || isLoadingTransaction,
                loading: isLoading || isLoadingTransaction
              }
            }
          : undefined
      }
    >
      <H2>{I18n.t("wallet.payment.methodSelection.header")}</H2>
      <VSpacer size={16} />
      {isLoading ? (
        <CheckoutPaymentMethodsListSkeleton />
      ) : (
        <CheckoutPaymentMethodsList />
      )}
    </IOScrollView>
  );
};

export { WalletPaymentPickMethodScreen };