diff --git a/img/features/wallet/transaction-receipt-divider.svg b/img/features/wallet/transaction-receipt-divider.svg new file mode 100644 index 00000000000..292f1a0b753 --- /dev/null +++ b/img/features/wallet/transaction-receipt-divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/locales/en/index.yml b/locales/en/index.yml index 050fdb203e0..8b3ac755763 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3788,3 +3788,20 @@ fastLogin: description: every time a login with SPID or CIE is performed, we'll send you an e-mail to let you know. whatsNew: title: What's changed? +transaction: + details: + title: Dettaglio operazione + totalAmount: Totale + totalFee: Il totale comprende + totalFeePsp: di commissione, applicata da {{pspName}} + info: + title: Informazioni sulla transazione + pspName: Gestore della transazione (PSP) + dateAndHour: Data e ora + transactionId: ID transazione + operation: + amount: Importo + creditor: Ente creditore + debtor: Debitore + iuv: IUV + subject: Oggetto del pagamento diff --git a/locales/it/index.yml b/locales/it/index.yml index 808555b8750..2d966ecc750 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3788,3 +3788,20 @@ fastLogin: description: a ogni nuovo accesso con SPID o CIE, ti invieremo un’email. whatsNew: title: Cosa cambia? +transaction: + details: + title: Dettaglio operazione + totalAmount: Totale + totalFee: Il totale comprende + totalFeePsp: di commissione, applicata da {{pspName}} + info: + title: Informazioni sulla transazione + pspName: Gestore della transazione (PSP) + dateAndHour: Data e ora + transactionId: ID transazione + operation: + amount: Importo + creditor: Ente creditore + debtor: Debitore + iuv: IUV + subject: Oggetto del pagamento diff --git a/ts/components/ui/RNavScreenWithLargeHeader.tsx b/ts/components/ui/RNavScreenWithLargeHeader.tsx index d901ea0a5ad..c570dfcf176 100644 --- a/ts/components/ui/RNavScreenWithLargeHeader.tsx +++ b/ts/components/ui/RNavScreenWithLargeHeader.tsx @@ -1,8 +1,4 @@ -import { - H3, - HeaderSecondLevel, - IOVisualCostants -} from "@pagopa/io-app-design-system"; +import { H2, HeaderSecondLevel, IOStyles } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; import React, { ComponentProps, useLayoutEffect, useState } from "react"; import { LayoutChangeEvent, View } from "react-native"; @@ -79,7 +75,7 @@ export const RNavScreenWithLargeHeader = ({ - -

{title}

+ +

{title}

{children}
diff --git a/ts/features/walletV3/common/saga/index.ts b/ts/features/walletV3/common/saga/index.ts index a3bc5020ea7..a68ceb485c4 100644 --- a/ts/features/walletV3/common/saga/index.ts +++ b/ts/features/walletV3/common/saga/index.ts @@ -9,6 +9,7 @@ import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persiste import { createWalletClient } from "../api/client"; import { watchWalletOnboardingSaga } from "../../onboarding/saga"; import { watchWalletDetailsSaga } from "../../details/saga"; +import { watchWalletTransactionSaga } from "../../transaction/saga"; export function* watchWalletV3Saga(bpdToken: string): SagaIterator { const isPagoPATestEnabled = yield* select(isPagoPATestEnabledSelector); @@ -23,4 +24,6 @@ export function* watchWalletV3Saga(bpdToken: string): SagaIterator { yield* fork(watchWalletOnboardingSaga, client, token); yield* fork(watchWalletDetailsSaga, client, token); + + yield* fork(watchWalletTransactionSaga, client, token); } diff --git a/ts/features/walletV3/common/store/actions/index.ts b/ts/features/walletV3/common/store/actions/index.ts index 886d2f6f31e..7c991d4fd70 100644 --- a/ts/features/walletV3/common/store/actions/index.ts +++ b/ts/features/walletV3/common/store/actions/index.ts @@ -1,4 +1,8 @@ import { WalletDetailsActions } from "../../../details/store/actions"; import { WalletOnboardingActions } from "../../../onboarding/store/actions"; +import { WalletTransactionActions } from "../../../transaction/store/actions"; -export type WalletV3Actions = WalletOnboardingActions | WalletDetailsActions; +export type WalletV3Actions = + | WalletOnboardingActions + | WalletDetailsActions + | WalletTransactionActions; diff --git a/ts/features/walletV3/common/store/reducers/index.ts b/ts/features/walletV3/common/store/reducers/index.ts index c6778fa3947..90d36d49f2c 100644 --- a/ts/features/walletV3/common/store/reducers/index.ts +++ b/ts/features/walletV3/common/store/reducers/index.ts @@ -5,15 +5,20 @@ import walletOnboardingReducer, { import walletDetailsReducer, { WalletDetailsState } from "../../../details/store"; +import walletTransactionReducer, { + WalletTransactionState +} from "../../../transaction/store"; export type WalletV3State = { onboarding: WalletOnboardingState; details: WalletDetailsState; + transaction: WalletTransactionState; }; const walletV3Reducer = combineReducers({ onboarding: walletOnboardingReducer, - details: walletDetailsReducer + details: walletDetailsReducer, + transaction: walletTransactionReducer }); export default walletV3Reducer; diff --git a/ts/features/walletV3/transaction/components/WalletTransactionDetailsList.tsx b/ts/features/walletV3/transaction/components/WalletTransactionDetailsList.tsx new file mode 100644 index 00000000000..0b9939618f5 --- /dev/null +++ b/ts/features/walletV3/transaction/components/WalletTransactionDetailsList.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import { View } from "react-native"; +import Placeholder from "rn-placeholder"; +import { + Divider, + HSpacer, + IOStyles, + ListItemTransaction, + VSpacer +} from "@pagopa/io-app-design-system"; +import { Transaction } from "../../../../types/pagopa"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { Dettaglio } from "../../../../../definitions/pagopa/Dettaglio"; +import { + cleanTransactionDescription, + getTransactionIUV +} from "../../../../utils/payment"; + +type Props = { + transaction?: Transaction | null; + loading: boolean; + onPress: (operationDetails: Dettaglio) => void; +}; + +/** + * This component renders a list of transaction details which currently is just a single item + * TODO: Using the actual information, this component is already arranged to handle a list that will be implemented from the BIZ Event implementation (https://pagopa.atlassian.net/browse/IOBP-440) + */ +export const WalletTransactionDetailsList = ({ + transaction, + loading, + onPress +}: Props) => { + if (loading) { + return ; + } + if (!transaction) { + return <>; + } + + const operationDetails: Dettaglio = { + importo: transaction.amount.amount, + enteBeneficiario: transaction.merchant, + IUV: pipe(getTransactionIUV(transaction.description), O.toUndefined) + }; + + return ( + <> + onPress?.(operationDetails)} + /> + + + ); +}; + +const SkeletonTransactionDetailsList = () => ( + + + + + + + + + + +); diff --git a/ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx b/ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx new file mode 100644 index 00000000000..30534267430 --- /dev/null +++ b/ts/features/walletV3/transaction/components/WalletTransactionHeadingSection.tsx @@ -0,0 +1,93 @@ +import { useNavigation } from "@react-navigation/native"; +import Placeholder from "rn-placeholder"; +import React from "react"; +import { View } from "react-native"; +import { Body, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import { Psp, Transaction } from "../../../../types/pagopa"; +import { Dettaglio } from "../../../../../definitions/pagopa/Dettaglio"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import I18n from "../../../../i18n"; +import { + WalletTransactionRoutes, + WalletTransactionStackNavigation +} from "../navigation/navigator"; + +import { WalletTransactionTotalAmount } from "./WalletTransactionTotalAmount"; +import { WalletTransactionDetailsList } from "./WalletTransactionDetailsList"; + +type Props = { + transaction?: Transaction; + psp?: Psp; + loading: boolean; +}; + +export const WalletTransactionHeadingSection = ({ + transaction, + psp, + loading +}: Props) => { + const navigation = useNavigation(); + + const handlePressTransactionDetails = (operationDetails: Dettaglio) => { + if (transaction) { + navigation.navigate( + WalletTransactionRoutes.WALLET_TRANSACTION_OPERATION_DETAILS, + { + operationDetails, + operationSubject: transaction.description, + operationName: transaction.description + } + ); + } + }; + + const FeeAmountSection = () => { + if (psp && transaction?.fee && !loading) { + const formattedFee = formatNumberCentsToAmount( + transaction.fee.amount, + true, + "right" + ); + return ( + + {I18n.t("transaction.details.totalFee")}{" "} + {formattedFee}{" "} + {I18n.t("transaction.details.totalFeePsp", { + pspName: psp.businessName || "" + })} + . + + ); + } + if (loading) { + return ( + + + + + + + ); + } + return <>; + }; + + return ( + + + + + + + + + + ); +}; diff --git a/ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx b/ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx new file mode 100644 index 00000000000..b437782e97a --- /dev/null +++ b/ts/features/walletV3/transaction/components/WalletTransactionInfoSection.tsx @@ -0,0 +1,117 @@ +/* eslint-disable functional/immutable-data */ +import * as React from "react"; +import { StyleSheet, View } from "react-native"; +import Placeholder from "rn-placeholder"; +import { + Divider, + IORadiusScale, + IOVisualCostants, + ListItemHeader, + ListItemInfo, + ListItemInfoCopy, + VSpacer +} from "@pagopa/io-app-design-system"; +import { IOStyles } from "../../../../components/core/variables/IOStyles"; +import I18n from "../../../../i18n"; +import { Psp, Transaction } from "../../../../types/pagopa"; +import { format } from "../../../../utils/dates"; +import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard"; +import TransactionReceiptDivider from "../../../../../img/features/wallet/transaction-receipt-divider.svg"; + +type WalletTransactionInfoSectionProps = { + transaction?: Transaction; + psp?: Psp; + loading?: boolean; +}; + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + ...IOStyles.horizontalContentPadding + }, + contentCard: { + ...IOStyles.horizontalContentPadding, + ...IOStyles.bgWhite, + borderRadius: IORadiusScale["1"], + marginVertical: IOVisualCostants.appMarginDefault + } +}); + +/** + * Component that shows a success message after the wallet onboarding process is completed + * TODO: Define the desired design of this component + */ +const WalletTransactionInfoSection = ({ + transaction, + psp, + loading +}: WalletTransactionInfoSectionProps) => ( + <> + + + + + {loading && ( + <> + + + + + + + )} + {!loading && transaction && ( + <> + {psp?.businessName && ( + <> + + + + )} + + + + clipboardSetStringWithFeedback(transaction.id.toString()) + } + accessibilityLabel={I18n.t( + "transaction.details.info.transactionId" + )} + label={I18n.t("transaction.details.info.transactionId")} + value={transaction.id.toString()} + /> + + )} + + + +); + +const SkeletonItem = () => ( + + + + + +); + +export default WalletTransactionInfoSection; diff --git a/ts/features/walletV3/transaction/components/WalletTransactionTotalAmount.tsx b/ts/features/walletV3/transaction/components/WalletTransactionTotalAmount.tsx new file mode 100644 index 00000000000..08dbbb2d329 --- /dev/null +++ b/ts/features/walletV3/transaction/components/WalletTransactionTotalAmount.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import Placeholder from "rn-placeholder"; +import { StyleSheet, View } from "react-native"; +import { H3, H6, IOStyles } from "@pagopa/io-app-design-system"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import I18n from "../../../../i18n"; + +type TotalAmountSectionProps = { + totalAmount?: number; + loading?: boolean; +}; + +const styles = StyleSheet.create({ + container: { + ...IOStyles.rowSpaceBetween, + ...IOStyles.alignCenter, + ...IOStyles.flex + } +}); + +export const WalletTransactionTotalAmount = ({ + totalAmount, + loading +}: TotalAmountSectionProps) => ( + +
{I18n.t("transaction.details.totalAmount")}
+ {loading && ( + + + + )} + {!loading && totalAmount && ( +

{formatNumberCentsToAmount(totalAmount, true, "right")}

+ )} +
+); diff --git a/ts/features/walletV3/transaction/navigation/navigator.tsx b/ts/features/walletV3/transaction/navigation/navigator.tsx new file mode 100644 index 00000000000..402c2f449ff --- /dev/null +++ b/ts/features/walletV3/transaction/navigation/navigator.tsx @@ -0,0 +1,57 @@ +import { ParamListBase } from "@react-navigation/native"; +import { + createStackNavigator, + StackNavigationProp +} from "@react-navigation/stack"; +import React from "react"; +import { isGestureEnabled } from "../../../../utils/navigation"; +import WalletTransactionDetailsScreen, { + WalletTransactionDetailsScreenParams +} from "../screens/WalletTransactionDetailsScreen"; +import WalletTransactionOperationDetailsScreen, { + WalletTransactionOperationDetailsScreenParams +} from "../screens/WalletTransactionOperationDetails"; + +export const WalletTransactionRoutes = { + WALLET_TRANSACTION_MAIN: "WALLET_TRANSACTION_MAIN", + WALLET_TRANSACTION_DETAILS: "WALLET_TRANSACTION_DETAILS", + WALLET_TRANSACTION_OPERATION_DETAILS: "WALLET_TRANSACTION_OPERATION_DETAILS" +} as const; + +export type WalletTransactionParamsList = { + [WalletTransactionRoutes.WALLET_TRANSACTION_MAIN]: undefined; + [WalletTransactionRoutes.WALLET_TRANSACTION_DETAILS]: WalletTransactionDetailsScreenParams; + [WalletTransactionRoutes.WALLET_TRANSACTION_OPERATION_DETAILS]: WalletTransactionOperationDetailsScreenParams; +}; + +const Stack = createStackNavigator(); + +export const WalletTransactionNavigator = () => ( + + + + +); + +export type WalletTransactionStackNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string +> = StackNavigationProp; + +export type WalletTransactionStackNavigation = + WalletTransactionStackNavigationProp< + WalletTransactionParamsList, + keyof WalletTransactionParamsList + >; diff --git a/ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts b/ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts new file mode 100644 index 00000000000..db9e4dd858d --- /dev/null +++ b/ts/features/walletV3/transaction/saga/handleGetTransactionDetails.ts @@ -0,0 +1,39 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { put, select } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { walletTransactionDetailsGet } from "../store/actions"; +import { getTransactions } from "../../../../store/reducers/wallet/transactions"; +import { getGenericError } from "../../../../utils/errors"; + +/** + * Handle the remote call to get the transaction details + * TODO: This is a temporary implementation to simulate the BIZ Event API, it will be replaced as soon as the BIZ Event API will be available (https://pagopa.atlassian.net/browse/IOBP-440) + * @param getPaymentMethods + * @param action + */ +export function* handleGetTransactionDetails( + _getTransactionDetails: any, // TODO: Replace with the real type when the BIZ Event API will be available + _token: string, + action: ActionType<(typeof walletTransactionDetailsGet)["request"]> +) { + // TODO: Add the whole logic here to call the BIZ Event API as soon as it will be available and replace the following code + const transactions = yield* select(getTransactions); + const transactionDetails = pot.toUndefined( + pot.map(transactions, transactions => + transactions.find(trx => trx.id === action.payload.transactionId) + ) + ); + if (transactionDetails) { + yield* put(walletTransactionDetailsGet.success(transactionDetails)); + return; + } + yield* put( + walletTransactionDetailsGet.failure({ + ...getGenericError( + new Error( + `Transaction details not found for transaction id ${action.payload.transactionId}` + ) + ) + }) + ); +} diff --git a/ts/features/walletV3/transaction/saga/index.ts b/ts/features/walletV3/transaction/saga/index.ts new file mode 100644 index 00000000000..e86a47517ae --- /dev/null +++ b/ts/features/walletV3/transaction/saga/index.ts @@ -0,0 +1,23 @@ +import { SagaIterator } from "redux-saga"; +import { takeLatest } from "typed-redux-saga/macro"; + +import { WalletClient } from "../../common/api/client"; +import { walletTransactionDetailsGet } from "../store/actions"; +import { handleGetTransactionDetails } from "./handleGetTransactionDetails"; + +/** + * Handle Wallet transaction requests + * @param bearerToken + */ +export function* watchWalletTransactionSaga( + walletClient: WalletClient, + token: string +): SagaIterator { + // TODO: Connect the saga code here to the BIZ Event API as asoon as it will be available (https://pagopa.atlassian.net/browse/IOBP-440) + yield* takeLatest( + walletTransactionDetailsGet.request, + handleGetTransactionDetails, + walletClient.getWalletById, // TODO: Add the get transaction details API call here when BIZ Event API will be available + token + ); +} diff --git a/ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx b/ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx new file mode 100644 index 00000000000..19727e6db2e --- /dev/null +++ b/ts/features/walletV3/transaction/screens/WalletTransactionDetailsScreen.tsx @@ -0,0 +1,108 @@ +import * as React from "react"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { Dimensions, StyleSheet, View } from "react-native"; +import { IOColors } from "@pagopa/io-app-design-system"; +import { RouteProp, useRoute } from "@react-navigation/native"; + +import { WalletTransactionParamsList } from "../navigation/navigator"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; +import { walletTransactionDetailsGet } from "../store/actions"; +import { + walletTransactionDetailsPotSelector, + walletTransactionDetailsSelector +} from "../store"; +import { fetchPsp } from "../../../../store/actions/wallet/transactions"; +import { Psp } from "../../../../types/pagopa"; +import WalletTransactionInfoSection from "../components/WalletTransactionInfoSection"; +import { WalletTransactionHeadingSection } from "../components/WalletTransactionHeadingSection"; +import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; +import I18n from "../../../../i18n"; + +export type WalletTransactionDetailsScreenParams = { + transactionId: number; +}; + +export type WalletTransactionDetailsScreenProps = RouteProp< + WalletTransactionParamsList, + "WALLET_TRANSACTION_DETAILS" +>; + +const windowHeight = Dimensions.get("window").height; + +const styles = StyleSheet.create({ + bottomBackground: { + position: "absolute", + height: windowHeight, + bottom: -windowHeight, + left: 0, + right: 0, + backgroundColor: IOColors["grey-50"] + }, + wrapper: { + flexGrow: 1, + alignContent: "flex-start", + backgroundColor: IOColors["grey-50"] + } +}); + +const WalletTransactionDetailsScreen = () => { + const dispatch = useIODispatch(); + const route = useRoute(); + const { transactionId } = route.params; + const transactionDetailsPot = useIOSelector( + walletTransactionDetailsPotSelector + ); + + const isLoading = pot.isLoading(transactionDetailsPot); + + const [transactionPsp, setTransactionPsp] = React.useState(); + + const transactionDetails = useIOSelector(walletTransactionDetailsSelector); + + useOnFirstRender(() => { + dispatch(walletTransactionDetailsGet.request({ transactionId })); + }); + + React.useEffect(() => { + if (transactionDetails && transactionDetails.idPsp) { + dispatch( + fetchPsp.request({ + idPsp: transactionDetails.idPsp, + onSuccess: ({ payload }) => { + setTransactionPsp(payload.psp); + } + }) + ); + } + }, [dispatch, transactionDetails]); + + return ( + + + + {/* The following line is used to show the background color gray that overlay the basic one which is white */} + + + + + + ); +}; + +export default WalletTransactionDetailsScreen; diff --git a/ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx b/ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx new file mode 100644 index 00000000000..92a1db7ab6d --- /dev/null +++ b/ts/features/walletV3/transaction/screens/WalletTransactionOperationDetails.tsx @@ -0,0 +1,123 @@ +import * as React from "react"; +import { ScrollView, StyleSheet } from "react-native"; +import { + Divider, + H6, + IOStyles, + ListItemInfo +} from "@pagopa/io-app-design-system"; +import { RouteProp, useRoute } from "@react-navigation/native"; + +import { WalletTransactionParamsList } from "../navigation/navigator"; +import { Dettaglio } from "../../../../../definitions/pagopa/Dettaglio"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { cleanTransactionDescription } from "../../../../utils/payment"; +import I18n from "../../../../i18n"; +import { RNavScreenWithLargeHeader } from "../../../../components/ui/RNavScreenWithLargeHeader"; + +const styles = StyleSheet.create({ + scrollViewContainer: { + ...IOStyles.flex, + ...IOStyles.horizontalContentPadding + } +}); + +export type WalletTransactionOperationDetailsScreenParams = { + operationName: string; + operationSubject: string; + operationDetails: Dettaglio; +}; + +export type WalletTransactionOperationDetailsScreenProps = RouteProp< + WalletTransactionParamsList, + "WALLET_TRANSACTION_OPERATION_DETAILS" +>; + +const WalletTransactionOperationDetailsScreen = () => { + const route = useRoute(); + const { operationDetails, operationName, operationSubject } = route.params; + + const getDebtorText = () => { + const debtorNameLabel = operationDetails.nomePagatore ? ( +
{operationDetails.nomePagatore}
+ ) : ( + <> + ); + const debtorCodeLabel = operationDetails.codicePagatore ? ( +
({operationDetails.codicePagatore})
+ ) : ( + <> + ); + return ( + <> + {debtorNameLabel} + {debtorCodeLabel} + + ); + }; + + return ( + + + {operationDetails.importo && ( + + )} + + {operationDetails.enteBeneficiario && ( + <> + + + + )} + {(operationDetails.codicePagatore || operationDetails.nomePagatore) && ( + <> + + + + )} + {operationDetails.IUV && ( + <> + + + + )} + {operationSubject && ( + + )} + + + ); +}; + +export default WalletTransactionOperationDetailsScreen; diff --git a/ts/features/walletV3/transaction/store/actions/index.ts b/ts/features/walletV3/transaction/store/actions/index.ts new file mode 100644 index 00000000000..50f40edff17 --- /dev/null +++ b/ts/features/walletV3/transaction/store/actions/index.ts @@ -0,0 +1,18 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../../utils/errors"; +import { Transaction } from "../../../../../types/pagopa"; + +export type WalletTransactionDetailsPayload = { + transactionId: number; +}; + +export const walletTransactionDetailsGet = createAsyncAction( + "WALLET_TRANSACTION_DETAILS_REQUEST", + "WALLET_TRANSACTION_DETAILS_SUCCESS", + "WALLET_TRANSACTION_DETAILS_FAILURE", + "WALLET_TRANSACTION_DETAILS_CANCEL" +)(); + +export type WalletTransactionActions = ActionType< + typeof walletTransactionDetailsGet +>; diff --git a/ts/features/walletV3/transaction/store/index.ts b/ts/features/walletV3/transaction/store/index.ts new file mode 100644 index 00000000000..f0979f9c0b8 --- /dev/null +++ b/ts/features/walletV3/transaction/store/index.ts @@ -0,0 +1,68 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as _ from "lodash"; +import { createSelector } from "reselect"; +import { getType } from "typesafe-actions"; +import { Action } from "../../../../store/actions/types"; +import { NetworkError } from "../../../../utils/errors"; +import { GlobalState } from "../../../../store/reducers/types"; + +import { Transaction } from "../../../../types/pagopa"; +import { walletTransactionDetailsGet } from "./actions"; + +export type WalletTransactionState = { + details: pot.Pot; +}; + +const INITIAL_STATE: WalletTransactionState = { + details: pot.noneLoading +}; + +const walletTransactionReducer = ( + state: WalletTransactionState = INITIAL_STATE, + action: Action +): WalletTransactionState => { + switch (action.type) { + // GET TRANSACTION DETAILS + case getType(walletTransactionDetailsGet.request): + return { + ...state, + details: pot.toLoading(pot.none) + }; + case getType(walletTransactionDetailsGet.success): + return { + ...state, + details: pot.some(action.payload) + }; + case getType(walletTransactionDetailsGet.failure): + return { + ...state, + details: pot.toError(state.details, action.payload) + }; + case getType(walletTransactionDetailsGet.cancel): + return { + ...state, + details: pot.none + }; + } + return state; +}; + +const walletTransactionSelector = (state: GlobalState) => + state.features.wallet.transaction; + +export const walletTransactionDetailsPotSelector = createSelector( + walletTransactionSelector, + transaction => transaction.details +); + +export const walletTransactionDetailsSelector = createSelector( + walletTransactionDetailsPotSelector, + details => pot.toUndefined(pot.map(details, el => el)) +); + +export const isLoadingTransactionDetailsSelector = createSelector( + walletTransactionDetailsPotSelector, + details => pot.isLoading(details) +); + +export default walletTransactionReducer; diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index d7e75f447fa..8f2c279d6e7 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -55,6 +55,10 @@ import { WalletPaymentRoutes } from "../features/walletV3/payment/navigation/rou import { WalletPaymentBarcodeScanScreen } from "../features/walletV3/payment/screens/WalletPaymentBarcodeScanScreen"; import { ZendeskStackNavigator } from "../features/zendesk/navigation/navigator"; import ZENDESK_ROUTES from "../features/zendesk/navigation/routes"; +import { + WalletTransactionNavigator, + WalletTransactionRoutes +} from "../features/walletV3/transaction/navigation/navigator"; import { useIOSelector } from "../store/hooks"; import { isCdcEnabledSelector, @@ -285,6 +289,14 @@ const AuthenticatedStackNavigator = () => { ...hideHeaderOptions }} /> + {/* This screen is outside the WalletPaymentNavigator to enable the slide from bottom animation. FIXME IOBP-383: Using react-navigation 6.x we can achive this using a Stack.Group inside the WalletPaymentNavigator diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index 617a8d41781..9a42410f75d 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -51,6 +51,10 @@ import { WalletDetailsParamsList, WalletDetailsRoutes } from "../../features/walletV3/details/navigation/navigator"; +import { + WalletTransactionParamsList, + WalletTransactionRoutes +} from "../../features/walletV3/transaction/navigation/navigator"; import { WalletPaymentParamsList } from "../../features/walletV3/payment/navigation/params"; import { WalletPaymentRoutes } from "../../features/walletV3/payment/navigation/routes"; import { ZendeskParamsList } from "../../features/zendesk/navigation/params"; @@ -107,6 +111,7 @@ export type AppParamsList = { [WalletPaymentRoutes.WALLET_PAYMENT_MAIN]: NavigatorScreenParams; [WalletPaymentRoutes.WALLET_PAYMENT_BARCODE_SCAN]: undefined; // FIXME IOBP-383: remove after react-navigation 6.x upgrade. This should be insde WALLET_PAYMENT_MAIN [WalletDetailsRoutes.WALLET_DETAILS_MAIN]: NavigatorScreenParams; + [WalletTransactionRoutes.WALLET_TRANSACTION_MAIN]: NavigatorScreenParams; }; /** diff --git a/ts/screens/wallet/WalletHomeScreen.tsx b/ts/screens/wallet/WalletHomeScreen.tsx index aeeb6ee85ea..6cce2659799 100644 --- a/ts/screens/wallet/WalletHomeScreen.tsx +++ b/ts/screens/wallet/WalletHomeScreen.tsx @@ -86,7 +86,10 @@ import { isIdPayEnabledSelector } from "../../store/reducers/backendStatus"; import { paymentsHistorySelector } from "../../store/reducers/payments/history"; -import { isPagoPATestEnabledSelector } from "../../store/reducers/persistedPreferences"; +import { + isDesignSystemEnabledSelector, + isPagoPATestEnabledSelector +} from "../../store/reducers/persistedPreferences"; import { GlobalState } from "../../store/reducers/types"; import { creditCardAttemptsSelector } from "../../store/reducers/wallet/creditCard"; import { @@ -103,6 +106,7 @@ import customVariables from "../../theme/variables"; import { Transaction, Wallet } from "../../types/pagopa"; import { isStrictSome } from "../../utils/pot"; import { showToast } from "../../utils/showToast"; +import { WalletTransactionRoutes } from "../../features/walletV3/transaction/navigation/navigator"; export type WalletHomeNavigationParams = Readonly<{ newMethodAdded: boolean; @@ -432,6 +436,24 @@ class WalletHomeScreen extends React.PureComponent { this.props.loadTransactions(this.props.transactionsLoadedLength); }; + private navigateToWalletTransactionDetailsScreen = ( + transaction: Transaction + ) => { + if (this.props.isDesignSystemEnabled) { + this.props.navigation.navigate( + WalletTransactionRoutes.WALLET_TRANSACTION_MAIN, + { + screen: WalletTransactionRoutes.WALLET_TRANSACTION_DETAILS, + params: { + transactionId: transaction.id + } + } + ); + } else { + this.props.navigateToTransactionDetailsScreen(transaction); + } + }; + private transactionList( potTransactions: pot.Pot, Error> ) { @@ -442,7 +464,7 @@ class WalletHomeScreen extends React.PureComponent { areMoreTransactionsAvailable={this.props.areMoreTransactionsAvailable} onLoadMoreTransactions={this.handleLoadMoreTransactions} navigateToTransactionDetails={ - this.props.navigateToTransactionDetailsScreen + this.navigateToWalletTransactionDetailsScreen } ListEmptyComponent={this.listEmptyComponent()} /> @@ -556,6 +578,7 @@ const mapStateToProps = (state: GlobalState) => ({ bancomatListVisibleInWallet: bancomatListVisibleInWalletSelector(state), coBadgeListVisibleInWallet: cobadgeListVisibleInWalletSelector(state), bpdConfig: bpdRemoteConfigSelector(state), + isDesignSystemEnabled: isDesignSystemEnabledSelector(state), isIdPayEnabled: isIdPayEnabledSelector(state) }); diff --git a/ts/utils/stringBuilder.ts b/ts/utils/stringBuilder.ts index d350642d0f4..14ed286392d 100644 --- a/ts/utils/stringBuilder.ts +++ b/ts/utils/stringBuilder.ts @@ -15,13 +15,18 @@ export const centsToAmount = (cents: number): number => export const formatNumberAmount = ( amount: number, - displayCurrency: boolean = false + displayCurrency: boolean = false, + currencyPosition: "left" | "right" = "left" ): string => I18n.toCurrency(amount, { precision: DISPLAYED_DIGITS, delimiter: I18n.t("global.localization.delimiterSeparator"), separator: I18n.t("global.localization.decimalSeparator"), - format: displayCurrency ? "€ %n" : "%n" + format: displayCurrency + ? currencyPosition === "left" + ? "€ %n" + : "%n €" + : "%n" }); /** @@ -42,8 +47,10 @@ export const formatNumberWithNoDigits = ( export const formatNumberCentsToAmount = ( cents: number, - displayCurrency: boolean = false -): string => formatNumberAmount(centsToAmount(cents), displayCurrency); + displayCurrency: boolean = false, + currencyPosition: "left" | "right" = "left" +): string => + formatNumberAmount(centsToAmount(cents), displayCurrency, currencyPosition); export const buildExpirationDate = (creditCard: CardInfo): string => `${creditCard.expireMonth}/${creditCard.expireYear}`;