ts/screens/wallet/PaymentHistoryDetailsScreen.tsx
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as React from "react";
import { View } from "react-native";
import { connect } from "react-redux";
import { VSpacer, ButtonOutline } from "@pagopa/io-app-design-system";
import { useNavigation, useRoute, Route } from "@react-navigation/native";
import { EnteBeneficiario } from "../../../definitions/backend/EnteBeneficiario";
import { PaymentRequestsGetResponse } from "../../../definitions/backend/PaymentRequestsGetResponse";
import { ToolEnum } from "../../../definitions/content/AssistanceToolConfig";
import { ZendeskCategory } from "../../../definitions/content/ZendeskCategory";
import CopyButtonComponent from "../../components/CopyButtonComponent";
import { Body } from "../../components/core/typography/Body";
import { Label } from "../../components/core/typography/Label";
import { IOStyles } from "../../components/core/variables/IOStyles";
import ItemSeparatorComponent from "../../components/ItemSeparatorComponent";
import BaseScreenComponent from "../../components/screens/BaseScreenComponent";
import { getPaymentHistoryInfo } from "../../components/wallet/PaymentsHistoryList";
import {
paymentStatusType,
PaymentSummaryComponent
} from "../../components/wallet/PaymentSummaryComponent";
import { SlidedContentComponent } from "../../components/wallet/SlidedContentComponent";
import {
zendeskSelectedCategory,
zendeskSupportStart
} from "../../features/zendesk/store/actions";
import I18n from "../../i18n";
import {
IOStackNavigationProp,
IOStackNavigationRouteProps
} from "../../navigation/params/AppParamsList";
import { WalletParamsList } from "../../navigation/params/WalletParamsList";
import { Dispatch } from "../../store/actions/types";
import { canShowHelpSelector } from "../../store/reducers/assistanceTools";
import { assistanceToolConfigSelector } from "../../store/reducers/backendStatus";
import { PaymentHistory } from "../../store/reducers/payments/history";
import { isPaymentDoneSuccessfully } from "../../store/reducers/payments/utils";
import { GlobalState } from "../../store/reducers/types";
import { outcomeCodesSelector } from "../../store/reducers/wallet/outcomeCode";
import { Transaction } from "../../types/pagopa";
import { formatDateAsLocal } from "../../utils/dates";
import { maybeInnerProperty } from "../../utils/options";
import {
getCodiceAvviso,
getErrorDescriptionV2,
getPaymentHistoryDetails,
getPaymentOutcomeCodeDescription,
getTransactionFee
} from "../../utils/payment";
import { formatNumberCentsToAmount } from "../../utils/stringBuilder";
import { isStringNullyOrEmpty } from "../../utils/strings";
import {
addTicketCustomField,
appendLog,
assistanceToolRemoteConfig,
resetCustomFields,
zendeskCategoryId,
zendeskPaymentCategory,
zendeskPaymentFailure,
zendeskPaymentNav,
zendeskPaymentOrgFiscalCode,
zendeskPaymentStartOrigin
} from "../../utils/supportAssistance";
import { H2 } from "../../components/core/typography/H2";
export type PaymentHistoryDetailsScreenNavigationParams = Readonly<{
payment: PaymentHistory;
}>;
type Props = IOStackNavigationRouteProps<
WalletParamsList,
"PAYMENT_HISTORY_DETAIL_INFO"
> &
ReturnType<typeof mapStateToProps> &
ReturnType<typeof mapDispatchToProps>;
const notAvailable = I18n.t("global.remoteStates.notAvailable");
const renderItem = (label: string, value?: string) => {
if (isStringNullyOrEmpty(value)) {
return null;
}
return (
<React.Fragment>
<Body>{label}</Body>
<Label weight="Bold" color="bluegrey">
{value}
</Label>
<VSpacer size={16} />
</React.Fragment>
);
};
/**
* Payment Details
*/
class PaymentHistoryDetailsScreen extends React.Component<Props> {
private zendeskAssistanceLogAndStart = () => {
resetCustomFields();
// Set pagamenti_pagopa as category
addTicketCustomField(zendeskCategoryId, zendeskPaymentCategory.value);
// Add organization fiscal code custom field
addTicketCustomField(
zendeskPaymentOrgFiscalCode,
this.props.route.params.payment.data.organizationFiscalCode
);
if (this.props.route.params.payment.failure) {
// Add failure custom field
addTicketCustomField(
zendeskPaymentFailure,
this.props.route.params.payment.failure
);
}
// Add start origin custom field
addTicketCustomField(
zendeskPaymentStartOrigin,
this.props.route.params.payment.startOrigin
);
// Add rptId custom field
addTicketCustomField(
zendeskPaymentNav,
getCodiceAvviso(this.props.route.params.payment.data)
);
// Append the payment history details in the log
appendLog(getPaymentHistoryDetails(this.props.route.params.payment));
this.props.zendeskSupportWorkunitStart();
this.props.zendeskSelectedCategory(zendeskPaymentCategory);
};
private choosenTool = assistanceToolRemoteConfig(
this.props.assistanceToolConfig
);
private handleAskAssistance = () => {
switch (this.choosenTool) {
case ToolEnum.zendesk:
this.zendeskAssistanceLogAndStart();
break;
}
};
private getData = () => {
const payment = this.props.route.params.payment;
const codiceAvviso = getCodiceAvviso(payment.data);
const paymentCheckout = isPaymentDoneSuccessfully(payment);
const paymentInfo = getPaymentHistoryInfo(payment, paymentCheckout);
const paymentStatus: paymentStatusType = {
color: paymentInfo.color,
description: paymentInfo.text11
};
// the error could be on attiva or while the payment execution
// so the description is built first checking the attiva failure, alternatively
// it checks about the outcome if the payment went wrong
const errorDetail = pipe(
getErrorDescriptionV2(payment.failure),
O.fromNullable,
O.alt(() =>
pipe(
payment.outcomeCode,
O.fromNullable,
O.chain(oc =>
getPaymentOutcomeCodeDescription(oc, this.props.outcomeCodes)
)
)
)
);
const paymentOutcome = isPaymentDoneSuccessfully(payment);
const dateTime: string = `${formatDateAsLocal(
new Date(payment.started_at),
true,
true
)} - ${new Date(payment.started_at).toLocaleTimeString()}`;
const reason = pipe(
maybeInnerProperty<
PaymentRequestsGetResponse,
"causaleVersamento",
string
>(payment.verifiedData, "causaleVersamento", m => m),
O.fold(
() => notAvailable,
cv => cv
)
);
const recipient = pipe(
maybeInnerProperty<Transaction, "merchant", string>(
payment.transaction,
"merchant",
m => m
),
O.fold(
() => notAvailable,
c => c
)
);
const amount = maybeInnerProperty<Transaction, "amount", number>(
payment.transaction,
"amount",
m => m.amount
);
const grandTotal = maybeInnerProperty<Transaction, "grandTotal", number>(
payment.transaction,
"grandTotal",
m => m.amount
);
const idTransaction = pipe(
maybeInnerProperty<Transaction, "id", number>(
payment.transaction,
"id",
m => m
),
O.fold(
() => notAvailable,
id => `${id}`
)
);
const fee = getTransactionFee(payment.transaction);
const enteBeneficiario = pipe(
maybeInnerProperty<
PaymentRequestsGetResponse,
"enteBeneficiario",
EnteBeneficiario | undefined
>(payment.verifiedData, "enteBeneficiario", m => m),
O.toUndefined
);
const outcomeCode = payment.outcomeCode ?? "-";
return {
recipient,
reason,
enteBeneficiario,
codiceAvviso,
paymentOutcome,
paymentInfo,
paymentStatus,
dateTime,
outcomeCode,
amount,
fee,
grandTotal,
errorDetail,
idTransaction
};
};
private standardRow = (label: string, value: string) => (
<View style={[IOStyles.rowSpaceBetween, IOStyles.alignCenter]}>
<View style={IOStyles.flex}>
<Body>{label}</Body>
</View>
<Label weight="Bold" color="bluegreyDark">
{value}
</Label>
</View>
);
private renderSeparator = () => (
<React.Fragment>
<VSpacer size={24} />
<ItemSeparatorComponent noPadded={true} />
<VSpacer size={24} />
</React.Fragment>
);
/**
* This fragment is rendered only if {@link canShowHelp} is true
*/
private renderHelper = () => (
<View>
<View style={[IOStyles.horizontalContentPadding, IOStyles.alignCenter]}>
<Body>{I18n.t("payment.details.info.help")}</Body>
</View>
<VSpacer size={16} />
<ButtonOutline
fullWidth
icon="chat"
onPress={this.handleAskAssistance}
label={I18n.t("payment.details.info.buttons.help")}
accessibilityLabel={I18n.t("payment.details.info.buttons.help")}
/>
<VSpacer size={16} />
</View>
);
public render(): React.ReactNode {
const data = this.getData();
return (
<BaseScreenComponent
goBack={() => this.props.navigation.goBack()}
showChat={false}
dark={true}
headerTitle={I18n.t("payment.details.info.title")}
>
<SlidedContentComponent hasFlatBottom={true}>
{O.isSome(data.paymentOutcome) && data.paymentOutcome.value ? (
<PaymentSummaryComponent
title={I18n.t("payment.details.info.title")}
recipient={data.recipient}
description={data.reason}
paymentStatus={data.paymentStatus}
/>
) : (
<React.Fragment>
<PaymentSummaryComponent
title={I18n.t("payment.details.info.title")}
codiceAvviso={data.codiceAvviso}
paymentStatus={data.paymentStatus}
/>
{data.enteBeneficiario &&
renderItem(
I18n.t("payment.details.info.enteCreditore"),
`${data.enteBeneficiario.denominazioneBeneficiario}\n${data.enteBeneficiario.identificativoUnivocoBeneficiario}`
)}
{O.isSome(data.errorDetail) && (
<View key={"error"}>
<Body>{I18n.t("payment.errorDetails")}</Body>
<Body weight="Semibold" color="bluegreyDark">
{data.errorDetail.value}
</Body>
</View>
)}
</React.Fragment>
)}
<VSpacer size={4} />
{this.standardRow(
I18n.t("payment.details.info.outcomeCode"),
data.outcomeCode
)}
<VSpacer size={4} />
{this.standardRow(
I18n.t("payment.details.info.dateAndTime"),
data.dateTime
)}
{this.renderSeparator()}
{O.isSome(data.paymentOutcome) &&
data.paymentOutcome.value &&
O.isSome(data.amount) &&
O.isSome(data.grandTotal) && (
<React.Fragment>
{/** amount */}
{this.standardRow(
I18n.t("wallet.firstTransactionSummary.amount"),
formatNumberCentsToAmount(data.amount.value, true)
)}
{/** fee */}
{data.fee &&
this.standardRow(
I18n.t("wallet.firstTransactionSummary.fee"),
data.fee
)}
<VSpacer size={16} />
{/** total amount */}
<View style={[IOStyles.rowSpaceBetween, IOStyles.alignCenter]}>
<View style={IOStyles.flex}>
<H2>{I18n.t("wallet.firstTransactionSummary.total")}</H2>
</View>
<H2>
{formatNumberCentsToAmount(data.grandTotal.value, true)}
</H2>
</View>
{this.renderSeparator()}
{/** Transaction id */}
<View>
<Body>
{I18n.t("wallet.firstTransactionSummary.idTransaction")}
</Body>
<View
style={[IOStyles.rowSpaceBetween, IOStyles.alignCenter]}
>
<Body weight="Semibold" color="bluegreyDark">
{data.idTransaction}
</Body>
<CopyButtonComponent
textToCopy={data.idTransaction.toString()}
/>
</View>
</View>
<VSpacer size={40} />
</React.Fragment>
)}
{/* This check is redundant, since if the help can't be shown the user can't get there */}
{this.props.canShowHelp && this.renderHelper()}
</SlidedContentComponent>
</BaseScreenComponent>
);
}
}
const mapStateToProps = (state: GlobalState) => ({
outcomeCodes: outcomeCodesSelector(state),
assistanceToolConfig: assistanceToolConfigSelector(state),
canShowHelp: canShowHelpSelector(state)
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
// Start the assistance without FAQ ("n/a" is a placeholder)
zendeskSupportWorkunitStart: () =>
dispatch(
zendeskSupportStart({
startingRoute: "n/a",
assistanceForPayment: true,
assistanceForCard: false,
assistanceForFci: false
})
),
zendeskSelectedCategory: (category: ZendeskCategory) =>
dispatch(zendeskSelectedCategory(category))
});
const ConnectedPaymentHistoryDetailsScreen = connect(
mapStateToProps,
mapDispatchToProps
)(PaymentHistoryDetailsScreen);
const PaymentHistoryDetailsScreenFC = () => {
const navigation =
useNavigation<
IOStackNavigationProp<WalletParamsList, "PAYMENT_HISTORY_DETAIL_INFO">
>();
const route =
useRoute<
Route<
"PAYMENT_HISTORY_DETAIL_INFO",
PaymentHistoryDetailsScreenNavigationParams
>
>();
return (
<ConnectedPaymentHistoryDetailsScreen
navigation={navigation}
route={route}
/>
);
};
export default PaymentHistoryDetailsScreenFC;