From c4c37f65746dfcad14cb0791c2882787365bd9d1 Mon Sep 17 00:00:00 2001 From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:08:39 +0100 Subject: [PATCH 1/2] chore: [IOBP-935] Standardize error handling for user payment cancellation (#6358) ## Short description This pull request include adding a new error type for user payment cancellation on network failure. ## List of changes proposed in this pull request * Added a new error type `ValidationFaultPaymentGenericErrorAfterUserCancellationProblemJson` to handle errors after user cancellation. * Updated `WalletPaymentFailure` to include the new error type. * Modified the reducer to handle the new error type in `paymentsDeleteTransactionAction.failure`. ## How to test - In _Pagamenti_ - Press on _Paga un avviso_ - Insert payments data and go at the latest step (**DO NOT PRESS ON PAY**) - Press "Go Back" action repeatedly until a cancellation alert appears. - Dismiss payment --- .../payments/checkout/analytics/index.ts | 6 ++-- .../payments/checkout/store/reducers/index.ts | 18 ++++++++-- ...icErrorAfterUserCancellationProblemJson.ts | 34 +++++++++++++++++++ .../checkout/types/WalletPaymentFailure.ts | 12 ++++--- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 ts/features/payments/checkout/types/PaymentGenericErrorAfterUserCancellationProblemJson.ts diff --git a/ts/features/payments/checkout/analytics/index.ts b/ts/features/payments/checkout/analytics/index.ts index 3d85298d093..79c9edb9c09 100644 --- a/ts/features/payments/checkout/analytics/index.ts +++ b/ts/features/payments/checkout/analytics/index.ts @@ -74,9 +74,9 @@ export const getPaymentAnalyticsEventFromFailureOutcome = ( }; export const getPaymentAnalyticsEventFromRequestFailure = ( - falure: WalletPaymentFailure + failure: WalletPaymentFailure ) => { - switch (falure.faultCodeCategory) { + switch (failure.faultCodeCategory) { case "PAYMENT_UNAVAILABLE": return "PAYMENT_TECHNICAL_ERROR"; case "PAYMENT_DATA_ERROR": @@ -93,6 +93,8 @@ export const getPaymentAnalyticsEventFromRequestFailure = ( return "PAYMENT_ALREADY_PAID_ERROR"; case "PAYMENT_UNKNOWN": return "PAYMENT_NOT_FOUND_ERROR"; + case "PAYMENT_GENERIC_ERROR_AFTER_USER_CANCELLATION": + return "PAYMENT_GENERIC_ERROR_AFTER_USER_CANCELLATION"; default: return "PAYMENT_GENERIC_ERROR"; } diff --git a/ts/features/payments/checkout/store/reducers/index.ts b/ts/features/payments/checkout/store/reducers/index.ts index 314f0dbaa9b..0b05ca27981 100644 --- a/ts/features/payments/checkout/store/reducers/index.ts +++ b/ts/features/payments/checkout/store/reducers/index.ts @@ -9,12 +9,14 @@ import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/eco import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; +import { UserLastPaymentMethodResponse } from "../../../../../../definitions/pagopa/ecommerce/UserLastPaymentMethodResponse"; import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo"; import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets"; import { Action } from "../../../../../store/actions/types"; import { NetworkError } from "../../../../../utils/errors"; import { getSortedPspList } from "../../../common/utils"; import { WalletPaymentStepEnum } from "../../types"; +import { FaultCodeCategoryEnum } from "../../types/PaymentGenericErrorAfterUserCancellationProblemJson"; import { WalletPaymentFailure } from "../../types/WalletPaymentFailure"; import { paymentsCalculatePaymentFeesAction, @@ -34,7 +36,6 @@ import { selectPaymentPspAction, walletPaymentSetCurrentStep } from "../actions/orchestration"; -import { UserLastPaymentMethodResponse } from "../../../../../../definitions/pagopa/ecommerce/UserLastPaymentMethodResponse"; export const WALLET_PAYMENT_STEP_MAX = 4; export type PaymentsCheckoutState = { @@ -242,11 +243,24 @@ const reducer = ( transaction: pot.none }; case getType(paymentsGetPaymentTransactionInfoAction.failure): - case getType(paymentsDeleteTransactionAction.failure): return { ...state, transaction: pot.toError(state.transaction, action.payload) }; + case getType(paymentsDeleteTransactionAction.failure): + return { + ...state, + transaction: pot.toError( + state.transaction, + action.payload.kind === "generic" + ? { + faultCodeCategory: + FaultCodeCategoryEnum.PAYMENT_GENERIC_ERROR_AFTER_USER_CANCELLATION, + faultCodeDetail: "" + } + : action.payload + ) + }; // Authorization url case getType(paymentsStartPaymentAuthorizationAction.request): diff --git a/ts/features/payments/checkout/types/PaymentGenericErrorAfterUserCancellationProblemJson.ts b/ts/features/payments/checkout/types/PaymentGenericErrorAfterUserCancellationProblemJson.ts new file mode 100644 index 00000000000..5c8924ae311 --- /dev/null +++ b/ts/features/payments/checkout/types/PaymentGenericErrorAfterUserCancellationProblemJson.ts @@ -0,0 +1,34 @@ +import * as t from "io-ts"; +import { enumType } from "@pagopa/ts-commons/lib/types"; + +export enum FaultCodeCategoryEnum { + "PAYMENT_GENERIC_ERROR_AFTER_USER_CANCELLATION" = "PAYMENT_GENERIC_ERROR_AFTER_USER_CANCELLATION" +} + +// required attributes +const PaymentGenericErrorAfterUserCancellationProblemJsonR = t.type({ + faultCodeCategory: enumType( + FaultCodeCategoryEnum, + "faultCodeCategory" + ), + + faultCodeDetail: t.string +}); + +// optional attributes +const PaymentGenericErrorAfterUserCancellationProblemJsonO = t.partial({ + title: t.string +}); + +export const PaymentGenericErrorAfterUserCancellationProblemJson = + t.intersection( + [ + PaymentGenericErrorAfterUserCancellationProblemJsonR, + PaymentGenericErrorAfterUserCancellationProblemJsonO + ], + "PaymentGenericErrorAfterUserCancellationProblemJson" + ); + +export type PaymentGenericErrorAfterUserCancellationProblemJson = t.TypeOf< + typeof PaymentGenericErrorAfterUserCancellationProblemJson +>; diff --git a/ts/features/payments/checkout/types/WalletPaymentFailure.ts b/ts/features/payments/checkout/types/WalletPaymentFailure.ts index d8e5cd94a63..6f2fa0fe7ae 100644 --- a/ts/features/payments/checkout/types/WalletPaymentFailure.ts +++ b/ts/features/payments/checkout/types/WalletPaymentFailure.ts @@ -1,13 +1,14 @@ import * as t from "io-ts"; import { GatewayFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/GatewayFaultPaymentProblemJson"; import { PartyConfigurationFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PartyConfigurationFaultPaymentProblemJson"; -import { ValidationFaultPaymentUnknownProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentUnknownProblemJson"; -import { ValidationFaultPaymentDataErrorProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentDataErrorProblemJson"; +import { PaymentCanceledStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentCanceledStatusFaultPaymentProblemJson"; +import { PaymentDuplicatedStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentDuplicatedStatusFaultPaymentProblemJson"; import { PaymentExpiredStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentExpiredStatusFaultPaymentProblemJson"; import { PaymentOngoingStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentOngoingStatusFaultPaymentProblemJson"; -import { PaymentCanceledStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentCanceledStatusFaultPaymentProblemJson"; +import { ValidationFaultPaymentDataErrorProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentDataErrorProblemJson"; import { ValidationFaultPaymentUnavailableProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentUnavailableProblemJson"; -import { PaymentDuplicatedStatusFaultPaymentProblemJson } from "../../../../../definitions/pagopa/ecommerce/PaymentDuplicatedStatusFaultPaymentProblemJson"; +import { ValidationFaultPaymentUnknownProblemJson } from "../../../../../definitions/pagopa/ecommerce/ValidationFaultPaymentUnknownProblemJson"; +import { PaymentGenericErrorAfterUserCancellationProblemJson } from "./PaymentGenericErrorAfterUserCancellationProblemJson"; export type WalletPaymentFailure = t.TypeOf; export const WalletPaymentFailure = t.union([ @@ -19,5 +20,6 @@ export const WalletPaymentFailure = t.union([ PaymentOngoingStatusFaultPaymentProblemJson, PaymentCanceledStatusFaultPaymentProblemJson, ValidationFaultPaymentUnavailableProblemJson, - PaymentDuplicatedStatusFaultPaymentProblemJson + PaymentDuplicatedStatusFaultPaymentProblemJson, + PaymentGenericErrorAfterUserCancellationProblemJson ]); From 62246197f5df1cfaa9f0d233db3a932294d4ce7a Mon Sep 17 00:00:00 2001 From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:09:41 +0100 Subject: [PATCH 2/2] fix: [PE-732] CGN header snap point on scroll (#6357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short description This pull request updates to the `CgnMerchantsListByCategory` component optimizing the layout for better user experience ## List of changes proposed in this pull request - Adaptive padding calculation determined by the count of elements in a FlatList - Replace deprecated component `GenericErrorComponent` with `OperationResultScreenContent` ## How to test - Go to _Portafoglio_ - Tap on CGN - Tap on _Scopri le opportunità_ - In `ts/features/bonus/cgn/screens/merchants/CgnMerchantsListByCategory.tsx` replace `merchantsAll` with this mock data function: ```ts const mockData = (count: number) => Array.from({ length: count }, (_, i) => i.toString()).map(item => ({ id: item, name: "Merchant " + item, imageUrl: "https://via.placeholder.com/150", discounts: [ { id: "1", title: "Discount 1", description: "Description 1", discount: 10, discountType: "PERCENTAGE", discountValue: 10, startDate: "2021-06-01", endDate: "2021-06-30", termsAndConditions: "Terms and conditions 1", imageUrl: "https://via.placeholder.com/150" } ] })); ``` - After implementing the above changes, check the header behavior within the app to ensure it is functioning correctly. Make sure that it displays properly without any layout issues on scroll. ## Preview https://github.com/user-attachments/assets/0f6a38d5-3836-4273-a7d9-6a5e995e5c6a Co-authored-by: Alessandro --- .../merchants/CgnMerchantsListByCategory.tsx | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListByCategory.tsx b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListByCategory.tsx index 9fa30e8b65c..2df88c28664 100644 --- a/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListByCategory.tsx +++ b/ts/features/bonus/cgn/screens/merchants/CgnMerchantsListByCategory.tsx @@ -1,14 +1,3 @@ -import { Route, useNavigation, useRoute } 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 { useMemo } from "react"; -import { - View, - LayoutChangeEvent, - RefreshControl, - Platform -} from "react-native"; import { Divider, H3, @@ -19,22 +8,39 @@ import { VSpacer, hexToRgba } from "@pagopa/io-app-design-system"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Route, useNavigation, useRoute } 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 { useMemo } from "react"; +import { + Dimensions, + LayoutChangeEvent, + Platform, + RefreshControl, + View +} from "react-native"; import Animated, { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Merchant } from "../../../../../../definitions/cgn/merchants/Merchant"; -import { IOStyles } from "../../../../../components/core/variables/IOStyles"; -import GenericErrorComponent from "../../../../../components/screens/GenericErrorComponent"; -import I18n from "../../../../../i18n"; -import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../../../store/hooks"; +import { ProductCategoryEnum } from "../../../../../../definitions/cgn/merchants/ProductCategory"; import { getValueOrElse, isError, isLoading } from "../../../../../common/model/RemoteValue"; +import { IOStyles } from "../../../../../components/core/variables/IOStyles"; +import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent"; +import FocusAwareStatusBar from "../../../../../components/ui/FocusAwareStatusBar"; +import { useHeaderSecondLevel } from "../../../../../hooks/useHeaderSecondLevel"; +import I18n from "../../../../../i18n"; +import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../../store/hooks"; +import { CgnMerchantListSkeleton } from "../../components/merchants/CgnMerchantListSkeleton"; +import { CgnMerchantListViewRenderItem } from "../../components/merchants/CgnMerchantsListView"; import { CgnDetailsParamsList } from "../../navigation/params"; import CGN_ROUTES from "../../navigation/routes"; import { @@ -47,17 +53,13 @@ import { } from "../../store/reducers/merchants"; import { getCategorySpecs } from "../../utils/filters"; import { mixAndSortMerchants } from "../../utils/merchants"; -import { ProductCategoryEnum } from "../../../../../../definitions/cgn/merchants/ProductCategory"; -import { useHeaderSecondLevel } from "../../../../../hooks/useHeaderSecondLevel"; -import FocusAwareStatusBar from "../../../../../components/ui/FocusAwareStatusBar"; -import { CgnMerchantListViewRenderItem } from "../../components/merchants/CgnMerchantsListView"; -import { CgnMerchantListSkeleton } from "../../components/merchants/CgnMerchantListSkeleton"; export type CgnMerchantListByCategoryScreenNavigationParams = Readonly<{ category: ProductCategoryEnum; }>; const CgnMerchantsListByCategory = () => { + const screenHeight = Dimensions.get("window").height; const [titleHeight, setTitleHeight] = React.useState(0); const translationY = useSharedValue(0); @@ -72,6 +74,7 @@ const CgnMerchantsListByCategory = () => { // eslint-disable-next-line functional/immutable-data translationY.value = event.contentOffset.y; }); + const insets = useSafeAreaInsets(); const dispatch = useIODispatch(); const route = @@ -230,6 +233,17 @@ const CgnMerchantsListByCategory = () => { }} /> ); + + const getPaddingBottom = () => { + const ELEMENT_HEIGHT = 49; + const totalListElementsHeight = ELEMENT_HEIGHT * merchantsAll.length; + const usedVerticalSpace = + titleHeight + totalListElementsHeight + insets.bottom; + const availableVerticalSpace = screenHeight - usedVerticalSpace; + + return availableVerticalSpace < titleHeight ? availableVerticalSpace : 0; + }; + return ( <> { barStyle={"dark-content"} /> {isError(onlineMerchants) && isError(offlineMerchants) ? ( - + ) : ( { snapToEnd={false} contentContainerStyle={{ flexGrow: 1, - paddingBottom: 48, + paddingBottom: getPaddingBottom(), backgroundColor: IOColors.white }} refreshControl={refreshControl} - data={isListLoading && !isPullRefresh ? [] : merchantsAll} + data={merchantsAll} keyExtractor={item => item.id} ListEmptyComponent={CgnMerchantListSkeleton} renderItem={renderItem}