teamdigitale/italia-app

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

Summary

Maintainability
F
3 days
Test Coverage
import {
  Body,
  LabelLink,
  ListItemHeader,
  ModuleCheckout,
  VSpacer
} from "@pagopa/io-app-design-system";
import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { sequenceS } from "fp-ts/lib/Apply";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { useFocusEffect } from "@react-navigation/native";
import { default as React } from "react";
import { AmountEuroCents } from "../../../../../definitions/pagopa/ecommerce/AmountEuroCents";
import I18n from "../../../../i18n";
import { useIONavigation } from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder";
import { capitalize } from "../../../../utils/strings";
import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails";
import {
  WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL,
  getPaymentLogoFromWalletDetails
} from "../../common/utils";
import { WalletPaymentTotalAmount } from "../components/WalletPaymentTotalAmount";
import { useWalletPaymentAuthorizationModal } from "../hooks/useWalletPaymentAuthorizationModal";
import { PaymentsCheckoutRoutes } from "../navigation/routes";
import { walletPaymentSetCurrentStep } from "../store/actions/orchestration";
import {
  selectWalletPaymentCurrentStep,
  walletPaymentDetailsSelector
} from "../store/selectors";
import {
  walletPaymentSelectedPaymentMethodIdOptionSelector,
  walletPaymentSelectedPaymentMethodManagementOptionSelector,
  walletPaymentSelectedPaymentMethodOptionSelector,
  walletPaymentSelectedWalletIdOptionSelector,
  walletPaymentSelectedWalletOptionSelector
} from "../store/selectors/paymentMethods";
import {
  walletPaymentPspListSelector,
  walletPaymentSelectedPspSelector
} from "../store/selectors/psps";
import { walletPaymentTransactionSelector } from "../store/selectors/transaction";
import { WalletPaymentStepEnum } from "../types";
import {
  WalletPaymentOutcome,
  WalletPaymentOutcomeEnum
} from "../types/PaymentOutcomeEnum";
import { IOScrollView } from "../../../../components/ui/IOScrollView";
import * as analytics from "../analytics";
import { paymentAnalyticsDataSelector } from "../../history/store/selectors";

const WalletPaymentConfirmScreen = () => {
  const navigation = useIONavigation();

  const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector);
  const transactionPot = useIOSelector(walletPaymentTransactionSelector);
  const currentStep = useIOSelector(selectWalletPaymentCurrentStep);
  const selectedWalletIdOption = useIOSelector(
    walletPaymentSelectedWalletIdOptionSelector
  );
  const selectedPaymentMethodIdOption = useIOSelector(
    walletPaymentSelectedPaymentMethodIdOptionSelector
  );
  const selectedPaymentMethodManagement = useIOSelector(
    walletPaymentSelectedPaymentMethodManagementOptionSelector
  );
  const paymentAnalyticsData = useIOSelector(paymentAnalyticsDataSelector);

  const selectedPspOption = useIOSelector(walletPaymentSelectedPspSelector);

  const handleStartPaymentAuthorization = () =>
    pipe(
      sequenceS(O.Monad)({
        paymentDetail: pot.toOption(paymentDetailsPot),
        paymentMethodId: selectedPaymentMethodIdOption,
        selectedPsp: selectedPspOption,
        transaction: pot.toOption(transactionPot)
      }),
      O.map(({ paymentDetail, paymentMethodId, selectedPsp, transaction }) => {
        // In case of guest payment walletId could be undefined
        const walletId = O.toUndefined(selectedWalletIdOption);
        const paymentMethodManagement = O.toUndefined(
          selectedPaymentMethodManagement
        );
        const isAllCCP = pipe(
          transaction.payments[0],
          O.fromNullable,
          O.chainNullableK(payment => payment.isAllCCP),
          O.getOrElse(() => false)
        );

        analytics.trackPaymentConversion({
          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,
          saved_payment_method:
            paymentAnalyticsData?.savedPaymentMethods?.length || 0,
          selected_psp_flag: paymentAnalyticsData?.selectedPspFlag,
          data_entry: paymentAnalyticsData?.startOrigin
        });

        startPaymentAuthorizaton({
          paymentAmount: paymentDetail.amount as AmountEuroCents,
          paymentFees: (selectedPsp.taxPayerFee ?? 0) as AmountEuroCents,
          pspId: selectedPsp.idPsp ?? "",
          isAllCCP,
          transactionId: transaction.transactionId,
          walletId,
          paymentMethodId,
          paymentMethodManagement
        });
      })
    );

  const handleAuthorizationOutcome = React.useCallback(
    (outcome: WalletPaymentOutcome) => {
      navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, {
        screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME,
        params: {
          outcome
        }
      });
    },
    [navigation]
  );

  const {
    isLoading: isAuthUrlLoading,
    isError: isAuthUrlError,
    isPendingAuthorization,
    startPaymentAuthorizaton
  } = useWalletPaymentAuthorizationModal({
    onAuthorizationOutcome: handleAuthorizationOutcome
  });

  const isLoading = isAuthUrlLoading || isPendingAuthorization;
  const isError = isAuthUrlError;

  React.useEffect(() => {
    if (isError) {
      handleAuthorizationOutcome(WalletPaymentOutcomeEnum.GENERIC_ERROR);
    }
  }, [isError, handleAuthorizationOutcome]);

  useFocusEffect(
    React.useCallback(() => {
      if (currentStep !== WalletPaymentStepEnum.CONFIRM_TRANSACTION) {
        return;
      }
      analytics.trackPaymentSummaryScreen({
        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,
        saved_payment_method:
          paymentAnalyticsData?.savedPaymentMethods?.length || 0,
        selected_psp_flag: paymentAnalyticsData?.selectedPspFlag,
        payment_method_selected: paymentAnalyticsData?.selectedPaymentMethod
      });
      // should be called only when the current step is the confirm screen
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentStep])
  );

  const totalAmount = pipe(
    sequenceS(O.Monad)({
      taxFee: pipe(
        selectedPspOption,
        O.chainNullableK(({ taxPayerFee }) => taxPayerFee)
      ),
      paymentAmount: pipe(
        pot.toOption(paymentDetailsPot),
        O.map(({ amount }) => amount)
      )
    }),
    O.map(({ taxFee, paymentAmount }) => +paymentAmount + +taxFee),
    O.getOrElse(() => 0)
  );

  return (
    <IOScrollView
      actions={{
        type: "SingleButton",
        primary: {
          label: `${I18n.t("payment.confirm.pay")} ${formatNumberCentsToAmount(
            totalAmount,
            true,
            "right"
          )}`,
          accessibilityLabel: `${I18n.t(
            "payment.confirm.pay"
          )} ${formatNumberCentsToAmount(totalAmount, true, "right")}`,
          onPress: handleStartPaymentAuthorization,
          disabled: isLoading,
          loading: isLoading
        }
      }}
    >
      <ListItemHeader
        label={I18n.t("payment.confirm.payWith")}
        accessibilityLabel={I18n.t("payment.confirm.payWith")}
        iconName="creditCard"
      />
      <SelectedPaymentMethodModuleCheckout />
      <VSpacer size={24} />
      <ListItemHeader
        label={I18n.t("payment.confirm.fee")}
        accessibilityLabel={I18n.t("payment.confirm.fee")}
        iconName="psp"
      />
      <SelectedPspModuleCheckout />
      <VSpacer size={24} />
      <WalletPaymentTotalAmount totalAmount={totalAmount} />
      <VSpacer size={16} />
      <Body>
        {I18n.t("payment.confirm.termsAndConditions")}{" "}
        <LabelLink
          onPress={() =>
            openAuthenticationSession(
              WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL,
              "https"
            )
          }
        >
          {I18n.t("payment.confirm.termsAndConditionsLink")}
        </LabelLink>
      </Body>
    </IOScrollView>
  );
};

const SelectedPaymentMethodModuleCheckout = () => {
  const dispatch = useIODispatch();

  const selectedWalletOption = useIOSelector(
    walletPaymentSelectedWalletOptionSelector
  );
  const selectedPaymentMethodOption = useIOSelector(
    walletPaymentSelectedPaymentMethodOptionSelector
  );
  const paymentAnalyticsData = useIOSelector(paymentAnalyticsDataSelector);

  const handleOnPress = () => {
    analytics.trackPaymentSummaryEditing({
      payment_method_selected: paymentAnalyticsData?.selectedPaymentMethod,
      saved_payment_method:
        paymentAnalyticsData?.savedPaymentMethods?.length || 0,
      expiration_date: paymentAnalyticsData?.verifiedData?.dueDate,
      selected_psp_flag: paymentAnalyticsData?.selectedPspFlag,
      editing: "payment_method",
      amount: paymentAnalyticsData?.formattedAmount
    });
    dispatch(
      walletPaymentSetCurrentStep(WalletPaymentStepEnum.PICK_PAYMENT_METHOD)
    );
  };

  if (O.isSome(selectedWalletOption) && selectedWalletOption.value.details) {
    const details = selectedWalletOption.value.details;
    const paymentLogo = getPaymentLogoFromWalletDetails(details);
    const imageProps = {
      ...(paymentLogo !== undefined
        ? { paymentLogo }
        : { image: { uri: selectedWalletOption.value.paymentMethodAsset } })
    };

    return (
      <ModuleCheckout
        title={getPaymentTitle(details)}
        subtitle={getPaymentSubtitle(details)}
        ctaText={I18n.t("payment.confirm.editButton")}
        onPress={handleOnPress}
        {...imageProps}
      />
    );
  }

  if (O.isSome(selectedPaymentMethodOption)) {
    return (
      <ModuleCheckout
        title={selectedPaymentMethodOption.value.description}
        ctaText={I18n.t("payment.confirm.editButton")}
        onPress={handleOnPress}
        image={{ uri: selectedPaymentMethodOption.value.asset }}
      />
    );
  }

  return null;
};

const SelectedPspModuleCheckout = () => {
  const dispatch = useIODispatch();

  const pspListPot = useIOSelector(walletPaymentPspListSelector);
  const selectedPspOption = useIOSelector(walletPaymentSelectedPspSelector);
  const paymentAnalyticsData = useIOSelector(paymentAnalyticsDataSelector);
  const pspList = pot.getOrElse(pspListPot, []);
  const pspName = pipe(
    selectedPspOption,
    O.chainNullableK(({ pspBusinessName }) => pspBusinessName),
    O.getOrElse(() => "")
  );

  const taxFee = pipe(
    selectedPspOption,
    O.chainNullableK(({ taxPayerFee }) => taxPayerFee),
    O.getOrElse(() => 0)
  );

  const handleOnPress = () => {
    analytics.trackPaymentSummaryEditing({
      payment_method_selected: paymentAnalyticsData?.selectedPaymentMethod,
      saved_payment_method:
        paymentAnalyticsData?.savedPaymentMethods?.length || 0,
      expiration_date: paymentAnalyticsData?.verifiedData?.dueDate,
      selected_psp_flag: paymentAnalyticsData?.selectedPspFlag,
      editing: "psp",
      amount: paymentAnalyticsData?.formattedAmount
    });
    dispatch(walletPaymentSetCurrentStep(WalletPaymentStepEnum.PICK_PSP));
  };

  return (
    <ModuleCheckout
      ctaText={
        pspList.length > 1 ? I18n.t("payment.confirm.editButton") : undefined
      }
      title={formatNumberCentsToAmount(taxFee, true, "right")}
      subtitle={`${I18n.t("payment.confirm.feeAppliedBy")} ${pspName}`}
      onPress={handleOnPress}
    />
  );
};

const getPaymentSubtitle = (
  details: UIWalletInfoDetails
): string | undefined => {
  if (details.maskedEmail !== undefined) {
    return I18n.t("wallet.onboarding.paypal.name");
  } else if (details.maskedNumber !== undefined) {
    return `${details.bankName}`;
  }
  return undefined;
};

const getPaymentTitle = (details: UIWalletInfoDetails): string => {
  if (details.lastFourDigits !== undefined) {
    return `${capitalize(details.brand || "")} ••${details.lastFourDigits}`;
  } else if (details.maskedEmail !== undefined) {
    return `${details.maskedEmail}`;
  } else if (details.maskedNumber !== undefined) {
    return `${details.maskedNumber}`;
  }

  return "";
};

export { WalletPaymentConfirmScreen };