diff --git a/locales/de/index.yml b/locales/de/index.yml index 5fc1e27ba10..e7784037b80 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -1814,6 +1814,10 @@ wallet: PAYMENT_UNKNOWN: title: "Wir können die Zahlungsmitteilung nicht finden" subtitle: "Die Zahlungsmitteilung kann bereits bezahlt worden sein. Wende dich bitte an den ausstellende Körperschaft." + PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR: + title: "Die ausgewählte Zahlungsmethode ist nicht verfügbar" + subtitle: "Dies kann passieren, wenn der zu zahlende Betrag besonders hoch ist oder wenn du versuchst, eine digitale Steuer- oder Gebührmarke zu bezahlen." + action: "Wähle eine andere Zahlungsmethode" outcome: SUCCESS: title: "Du hast {{amount}} bezahlt" diff --git a/locales/en/index.yml b/locales/en/index.yml index 206be6486b7..3508a34dc4a 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -1906,6 +1906,10 @@ wallet: PAYMENT_UNKNOWN: title: "Non riusciamo a trovare l’avviso" subtitle: "L’avviso potrebbe essere stato già pagato. Per ricevere assistenza, contatta l’Ente Creditore che lo ha emesso." + PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR: + title: "The selected payment method is not available" + subtitle: "This can happen when the amount to be paid is particularly high, or if you are trying to pay a digital stamp duty." + action: "Select another payment method" outcome: SUCCESS: title: Hai pagato {{amount}} diff --git a/locales/it/index.yml b/locales/it/index.yml index 52c29c598c5..1b4214022d5 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -1423,6 +1423,7 @@ wallet: title: Non abbiamo trovato alcun BANCOMAT Pay attivo subtitle: "Contatta la tua Banca per attivare il servizio." primaryAction: Chiudi + alert: supportedCardPageLinkError: Si è verificato un errore durante l'apertura della pagina di riferimento per le carte supportate. msgErrorUpdateApp: "Si è verificato un errore durante l'apertura dello store delle app" @@ -1906,6 +1907,10 @@ wallet: PAYMENT_UNKNOWN: title: "Non riusciamo a trovare l’avviso" subtitle: "L’avviso potrebbe essere stato già pagato. Per ricevere assistenza, contatta l’Ente Creditore che lo ha emesso." + PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR: + title: "Il metodo di pagamento selezionato non è disponibile" + subtitle: "Può succedere quando l’importo da pagare è particolarmente elevato, o se stai cercando di pagare una marca da bollo digitale." + action: "Scegli un altro metodo" outcome: SUCCESS: title: Hai pagato {{amount}} diff --git a/ts/features/payments/checkout/analytics/index.ts b/ts/features/payments/checkout/analytics/index.ts index f3485f12c31..d9d0c9ed09b 100644 --- a/ts/features/payments/checkout/analytics/index.ts +++ b/ts/features/payments/checkout/analytics/index.ts @@ -353,3 +353,21 @@ export const trackPaymentStartFlow = ( }) ); }; + +export const trackPaymentsPspNotAvailableError = ( + props: Partial +) => { + void mixpanelTrack( + "PAYMENT_PSP_NOT_AVAILABLE_ERROR", + buildEventProperties("KO", "screen_view", props) + ); +}; + +export const trackPaymentsPspNotAvailableSelectNew = ( + props: Partial +) => { + void mixpanelTrack( + "PAYMENT_PSP_NOT_AVAILABLE_SELECT_NEW", + buildEventProperties("UX", "action", props) + ); +}; diff --git a/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx b/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx index 29466090901..796eb1540ba 100644 --- a/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentFailureDetail.tsx @@ -9,17 +9,19 @@ import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; -import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; -import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { paymentCompletedSuccess } from "../store/actions/orchestration"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; import { - selectOngoingPaymentHistory, - paymentAnalyticsDataSelector + paymentAnalyticsDataSelector, + selectOngoingPaymentHistory } from "../../history/store/selectors"; import * as analytics from "../analytics"; +import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; +import { PaymentsCheckoutRoutes } from "../navigation/routes"; +import { paymentsCalculatePaymentFeesAction } from "../store/actions/networking"; +import { paymentCompletedSuccess } from "../store/actions/orchestration"; import { selectWalletPaymentCurrentStep } from "../store/selectors"; +import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; import { getPaymentPhaseFromStep } from "../utils"; type Props = { @@ -51,6 +53,28 @@ const WalletPaymentFailureDetail = ({ failure }: Props) => { supportModal.present(); }; + const handleChangePaymentMethod = () => { + analytics.trackPaymentsPspNotAvailableSelectNew({ + amount: paymentAnalyticsData?.formattedAmount, + expiration_date: paymentAnalyticsData?.verifiedData?.dueDate, + organization_name: paymentAnalyticsData?.verifiedData?.paName, + organization_fiscal_code: + paymentAnalyticsData?.verifiedData?.paFiscalCode, + saved_payment_method: + paymentAnalyticsData?.savedPaymentMethods?.length || 0, + service_name: paymentAnalyticsData?.serviceName, + data_entry: paymentAnalyticsData?.startOrigin, + attempt: paymentAnalyticsData?.attempt, + payment_method_selected: paymentAnalyticsData?.selectedPaymentMethod, + selected_psp_flag: paymentAnalyticsData?.selectedPspFlag + }); + + dispatch(paymentsCalculatePaymentFeesAction.cancel()); + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_MAKE + }); + }; + const closeAction: OperationResultScreenContentProps["action"] = { label: I18n.t("global.buttons.close"), testID: "wallet-payment-failure-close-button", @@ -73,6 +97,18 @@ const WalletPaymentFailureDetail = ({ failure }: Props) => { secondaryAction: contactSupportAction }; + const selectOtherPaymentMethodAction: OperationResultScreenContentProps["action"] = + { + label: I18n.t( + "wallet.payment.failure.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR.action" + ), + testID: "wallet-payment-failure-go-back-button", + accessibilityLabel: I18n.t( + "wallet.payment.failure.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR.action" + ), + onPress: handleChangePaymentMethod + }; + const getPropsFromFailure = ({ faultCodeCategory }: WalletPaymentFailure): OperationResultScreenContentProps => { @@ -147,6 +183,18 @@ const WalletPaymentFailureDetail = ({ failure }: Props) => { action: closeAction, secondaryAction: contactSupportAction }; + case "PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR": + return { + pictogram: "cardIssue", + title: I18n.t( + "wallet.payment.failure.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR.title" + ), + subtitle: I18n.t( + "wallet.payment.failure.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR.subtitle" + ), + action: selectOtherPaymentMethodAction + }; + default: return genericErrorProps; } diff --git a/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts index 385fea32075..677c2e35409 100644 --- a/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts +++ b/ts/features/payments/checkout/saga/networking/handleWalletPaymentCalculateFees.ts @@ -10,6 +10,8 @@ import { readablePrivacyReport } from "../../../../../utils/reporters"; import { PaymentClient } from "../../../common/api/client"; import { paymentsCalculatePaymentFeesAction } from "../../store/actions/networking"; import { withPaymentsSessionToken } from "../../../common/utils/withPaymentsSessionToken"; +import * as analytics from "../../analytics"; +import { paymentAnalyticsDataSelector } from "../../../history/store/selectors"; export function* handleWalletPaymentCalculateFees( calculateFees: PaymentClient["calculateFeesForIO"], @@ -61,11 +63,29 @@ export function* handleWalletPaymentCalculateFees( return; } else if (res.status !== 401) { // The 401 status is handled by the withPaymentsSessionToken - yield* put( - paymentsCalculatePaymentFeesAction.failure( - getGenericError(new Error(`Error: ${res.status}`)) - ) - ); + if (res.status === 404) { + const paymentAnalyticsData = yield* select( + paymentAnalyticsDataSelector + ); + analytics.trackPaymentsPspNotAvailableError({ + organization_name: paymentAnalyticsData?.verifiedData?.paName, + organization_fiscal_code: + paymentAnalyticsData?.verifiedData?.paFiscalCode, + expiration_date: paymentAnalyticsData?.verifiedData?.dueDate, + attempt: paymentAnalyticsData?.attempt + }); + yield* put( + paymentsCalculatePaymentFeesAction.failure({ + kind: "notFound" + }) + ); + } else { + yield* put( + paymentsCalculatePaymentFeesAction.failure( + getGenericError(new Error(`Error: ${res.status}`)) + ) + ); + } } } } catch (e) { diff --git a/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx index 2d344179ccf..484c4719928 100644 --- a/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentPickMethodScreen.tsx @@ -5,10 +5,15 @@ 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 { IOScrollView } from "../../../../components/ui/IOScrollView"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; +import { PaymentAnalyticsSelectedMethodFlag } from "../../common/types/PaymentAnalytics"; +import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { paymentAnalyticsDataSelector } from "../../history/store/selectors"; +import * as analytics from "../analytics"; import { CheckoutPaymentMethodsList, CheckoutPaymentMethodsListSkeleton @@ -37,12 +42,6 @@ import { 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(); diff --git a/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx index 44cf8cff4ab..eef7105a732 100644 --- a/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx @@ -18,6 +18,8 @@ import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { getSortedPspList } from "../../common/utils"; +import { paymentAnalyticsDataSelector } from "../../history/store/selectors"; +import * as analytics from "../analytics"; import { WalletPspListSkeleton } from "../components/WalletPspListSkeleton"; import { useSortPspBottomSheet } from "../hooks/useSortPspBottomSheet"; import { PaymentsCheckoutRoutes } from "../navigation/routes"; @@ -25,15 +27,15 @@ import { selectPaymentPspAction, walletPaymentSetCurrentStep } from "../store/actions/orchestration"; +import { selectWalletPaymentCurrentStep } from "../store/selectors"; import { walletPaymentPspListSelector, walletPaymentSelectedPspSelector } from "../store/selectors/psps"; import { WalletPaymentPspSortType, WalletPaymentStepEnum } from "../types"; +import { FaultCodeCategoryEnum } from "../types/PspPaymentMethodNotAvailableProblemJson"; import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; -import * as analytics from "../analytics"; -import { paymentAnalyticsDataSelector } from "../../history/store/selectors"; -import { selectWalletPaymentCurrentStep } from "../store/selectors"; +import { WalletPaymentFailure } from "../types/WalletPaymentFailure"; const WalletPaymentPickPspScreen = () => { const dispatch = useIODispatch(); @@ -73,14 +75,26 @@ const WalletPaymentPickPspScreen = () => { React.useEffect(() => { if (isError) { - navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { - screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, - params: { - outcome: WalletPaymentOutcomeEnum.GENERIC_ERROR - } - }); + if ( + (pspListPot.error as WalletPaymentFailure)?.faultCodeCategory === + FaultCodeCategoryEnum.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR + ) { + navigation.navigate(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_FAILURE, + params: { + error: pspListPot.error + } + }); + } else { + navigation.replace(PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_NAVIGATOR, { + screen: PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_OUTCOME, + params: { + outcome: WalletPaymentOutcomeEnum.GENERIC_ERROR + } + }); + } } - }, [isError, navigation]); + }, [isError, navigation, pspListPot]); useFocusEffect( React.useCallback(() => { diff --git a/ts/features/payments/checkout/store/actions/networking.ts b/ts/features/payments/checkout/store/actions/networking.ts index 3303d4f05d1..08ec273e1e2 100644 --- a/ts/features/payments/checkout/store/actions/networking.ts +++ b/ts/features/payments/checkout/store/actions/networking.ts @@ -16,6 +16,8 @@ import { WalletPaymentFailure } from "../../types/WalletPaymentFailure"; import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo"; import { UserLastPaymentMethodResponse } from "../../../../../../definitions/pagopa/ecommerce/UserLastPaymentMethodResponse"; +type NotFound = { kind: "notFound" }; + export const paymentsGetPaymentDetailsAction = createAsyncAction( "PAYMENTS_GET_PAYMENT_DETAILS_REQUEST", "PAYMENTS_GET_PAYMENT_DETAILS_SUCCESS", @@ -56,11 +58,13 @@ export type CalculateFeePayload = { export const paymentsCalculatePaymentFeesAction = createAsyncAction( "PAYMENTS_CALCULATE_PAYMENT_FEES_REQUEST", "PAYMENTS_CALCULATE_PAYMENT_FEES_SUCCESS", - "PAYMENTS_CALCULATE_PAYMENT_FEES_FAILURE" + "PAYMENTS_CALCULATE_PAYMENT_FEES_FAILURE", + "PAYMENTS_CALCULATE_PAYMENT_FEES_CANCEL" )< CalculateFeeRequest & CalculateFeePayload, CalculateFeeResponse, - NetworkError + NetworkError | NotFound, + undefined >(); export type WalletPaymentCreateTransactionPayload = { diff --git a/ts/features/payments/checkout/store/reducers/index.ts b/ts/features/payments/checkout/store/reducers/index.ts index 0b05ca27981..26af6dd1e9f 100644 --- a/ts/features/payments/checkout/store/reducers/index.ts +++ b/ts/features/payments/checkout/store/reducers/index.ts @@ -17,6 +17,7 @@ import { NetworkError } from "../../../../../utils/errors"; import { getSortedPspList } from "../../../common/utils"; import { WalletPaymentStepEnum } from "../../types"; import { FaultCodeCategoryEnum } from "../../types/PaymentGenericErrorAfterUserCancellationProblemJson"; +import { FaultCodeCategoryEnum as PaymentMethodNotAvailableEnum } from "../../types/PspPaymentMethodNotAvailableProblemJson"; import { WalletPaymentFailure } from "../../types/WalletPaymentFailure"; import { paymentsCalculatePaymentFeesAction, @@ -48,7 +49,7 @@ export type PaymentsCheckoutState = { userWallets: pot.Pot; recentUsedPaymentMethod: pot.Pot; allPaymentMethods: pot.Pot; - pspList: pot.Pot, NetworkError>; + pspList: pot.Pot, NetworkError | WalletPaymentFailure>; selectedWallet: O.Option; selectedPaymentMethod: O.Option; selectedPsp: O.Option; @@ -210,10 +211,26 @@ const reducer = ( currentStep, selectedPsp }; + case getType(paymentsCalculatePaymentFeesAction.cancel): + return { + ...state, + pspList: pot.none, + selectedPaymentMethod: O.none, + currentStep: WalletPaymentStepEnum.PICK_PAYMENT_METHOD + }; case getType(paymentsCalculatePaymentFeesAction.failure): return { ...state, - pspList: pot.toError(state.pspList, action.payload) + pspList: pot.toError( + state.pspList, + action.payload.kind === "notFound" + ? { + faultCodeCategory: + PaymentMethodNotAvailableEnum.PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR, + faultCodeDetail: "" + } + : action.payload + ) }; case getType(selectPaymentPspAction): diff --git a/ts/features/payments/checkout/types/PspPaymentMethodNotAvailableProblemJson.ts b/ts/features/payments/checkout/types/PspPaymentMethodNotAvailableProblemJson.ts new file mode 100644 index 00000000000..5d1a9a3c3d0 --- /dev/null +++ b/ts/features/payments/checkout/types/PspPaymentMethodNotAvailableProblemJson.ts @@ -0,0 +1,33 @@ +import * as t from "io-ts"; +import { enumType } from "@pagopa/ts-commons/lib/types"; + +export enum FaultCodeCategoryEnum { + "PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR" = "PSP_PAYMENT_METHOD_NOT_AVAILABLE_ERROR" +} + +// required attributes +const PspPaymentMethodNotAvailableProblemJsonR = t.type({ + faultCodeCategory: enumType( + FaultCodeCategoryEnum, + "faultCodeCategory" + ), + + faultCodeDetail: t.string +}); + +// optional attributes +const PspPaymentMethodNotAvailableProblemJsonO = t.partial({ + title: t.string +}); + +export const PspPaymentMethodNotAvailableProblemJson = t.intersection( + [ + PspPaymentMethodNotAvailableProblemJsonR, + PspPaymentMethodNotAvailableProblemJsonO + ], + "PspPaymentMethodNotAvailableProblemJson" +); + +export type PspPaymentMethodNotAvailableProblemJson = t.TypeOf< + typeof PspPaymentMethodNotAvailableProblemJson +>; diff --git a/ts/features/payments/checkout/types/WalletPaymentFailure.ts b/ts/features/payments/checkout/types/WalletPaymentFailure.ts index 9aa4e107733..9a3b9118182 100644 --- a/ts/features/payments/checkout/types/WalletPaymentFailure.ts +++ b/ts/features/payments/checkout/types/WalletPaymentFailure.ts @@ -10,6 +10,7 @@ import { ValidationFaultPaymentUnavailableProblemJson } from "../../../../../def import { ValidationFaultPaymentUnknownProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentUnknownProblemJson"; import { PaymentGenericErrorAfterUserCancellationProblemJson } from "./PaymentGenericErrorAfterUserCancellationProblemJson"; import { PaymentVerifyGenericErrorProblemJson } from "./PaymentVerifyGenericErrorProblemJson"; +import { PspPaymentMethodNotAvailableProblemJson } from "./PspPaymentMethodNotAvailableProblemJson"; export type WalletPaymentFailure = t.TypeOf; export const WalletPaymentFailure = t.union([ @@ -23,5 +24,6 @@ export const WalletPaymentFailure = t.union([ ValidationFaultPaymentUnavailableProblemJson, PaymentDuplicatedStatusFaultPaymentProblemJson, PaymentGenericErrorAfterUserCancellationProblemJson, - PaymentVerifyGenericErrorProblemJson + PaymentVerifyGenericErrorProblemJson, + PspPaymentMethodNotAvailableProblemJson ]);