Skip to content

Commit

Permalink
chore: [IOBP-438] Add abort dialog for payment flow (#5379)
Browse files Browse the repository at this point in the history
## Short description
This PR adds an abort dialog to exit the payment flow.
The dialog is displayed only if the user has already created a
transaction (and locked the debt position) and, if the user confirms he
wants to exit the payment flow, the transaction is deleted and the debt
position unlocked.

## List of changes proposed in this pull request
- Added required locale keys
- Added `useWalletPaymentGoBackHandler` hook, which handles the dialog
logic
- Added reducer and saga to handle transaction cancellation
- Fixed an issue with the header in `WalletPaymentDetailScreen`

## How to test
Using the io-dev-api-server, navigate to the Wallet Payment playground
and start a new payment flow. Check that the dialog is displayed only
before a transaction is created (3rd step).

## Preview


https://github.com/pagopa/io-app/assets/6160324/7f6f66e8-92f0-4581-a38a-97f10634a3ba

---------

Co-authored-by: Alessandro Izzo <[email protected]>
  • Loading branch information
mastro993 and Hantex9 authored Jan 11, 2024
1 parent 6a0670d commit 2986e5c
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 33 deletions.
25 changes: 16 additions & 9 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ payment:
description: IO didn't store any payment attempts. Here you can find last payment attempts
title: Payment attempts
iuv: IUV payment {{iuv}}
confirm:
confirm:
totalAmount: Totale da pagare
pay: Paga
payWith: Paga con
Expand Down Expand Up @@ -1644,13 +1644,6 @@ wallet:
cancel: Keep adding
success: Problems while adding your card? Please contact your bank.
payment:
methodSelection:
header: Seleziona un metodo
yourMethods: I tuoi metodi
otherMethods: Altri metodi
alert:
body: "A causa dell’importo elevato, alcuni metodi non sono disponibili."
cta: "Ok, ho capito!"
barcodes:
choice:
title: Sono stati rilevati più codici. Quale vuoi usare?
Expand All @@ -1663,6 +1656,17 @@ wallet:
title: Inserisci il codice fiscale dell’Ente Creditore
subtitle: Ha 11 cifre, lo trovi vicino al codice QR.
placeholder: Codice fiscale Ente Creditore
abortDialog:
title: Vuoi interrompere l'operazione?
confirm: Sì, interrompi
cancel: No, torna indietro
methodSelection:
header: Seleziona un metodo
yourMethods: I tuoi metodi
otherMethods: Altri metodi
alert:
body: "A causa dell’importo elevato, alcuni metodi non sono disponibili."
cta: "Ok, ho capito!"
psp:
title: Scegli chi gestirà il pagamento
description: Ogni gestore propone una commissione.
Expand All @@ -1671,13 +1675,16 @@ wallet:
defaultName: Gestore
pspTitle: Gestore
pspSortButton: Ordina
featuredReason: Perché sei già cliente
featuredReason: Perché sei già cliente
continueButton: Continua
sortBottomSheet:
default: Default
amount: Per importo
name: Per nome
outcome:
cancelled:
title: L’operazione è stata annullata
subtitle: Non è stato addebitato alcun importo.
success:
title: Hai pagato {{amount}}
button: Ok, chiudi
Expand Down
25 changes: 16 additions & 9 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ payment:
description: IO non ha memorizzato tentativi di pagamento. Qui troverai i tentativi di pagamento più recenti
title: Tentativi di pagamento
iuv: Codice IUV {{iuv}}
confirm:
confirm:
totalAmount: Totale da pagare
pay: Paga
payWith: Paga con
Expand Down Expand Up @@ -1644,13 +1644,6 @@ wallet:
cancel: No, continua
success: Problemi con l'aggiunta? Contatta la tua banca.
payment:
methodSelection:
header: Seleziona un metodo
yourMethods: I tuoi metodi
otherMethods: Altri metodi
alert:
body: "A causa dell’importo elevato, alcuni metodi non sono disponibili."
cta: "Ok, ho capito!"
barcodes:
choice:
title: Sono stati rilevati più codici. Quale vuoi usare?
Expand All @@ -1663,6 +1656,17 @@ wallet:
title: Inserisci il codice fiscale dell’Ente Creditore
subtitle: Ha 11 cifre, lo trovi vicino al codice QR.
placeholder: Codice fiscale Ente Creditore
abortDialog:
title: Vuoi interrompere l'operazione?
confirm: Sì, interrompi
cancel: No, torna indietro
methodSelection:
header: Seleziona un metodo
yourMethods: I tuoi metodi
otherMethods: Altri metodi
alert:
body: "A causa dell’importo elevato, alcuni metodi non sono disponibili."
cta: "Ok, ho capito!"
psp:
title: Scegli chi gestirà il pagamento
description: Ogni gestore propone una commissione.
Expand All @@ -1671,13 +1675,16 @@ wallet:
defaultName: Gestore
pspTitle: Gestore
pspSortButton: Ordina
featuredReason: Perché sei già cliente
featuredReason: Perché sei già cliente
continueButton: Continua
sortBottomSheet:
default: Default
amount: Per importo
name: Per nome
outcome:
cancelled:
title: L’operazione è stata annullata
subtitle: Non è stato addebitato alcun importo.
success:
title: Hai pagato {{amount}}
button: Ok, chiudi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as pot from "@pagopa/ts-commons/lib/pot";
import { useNavigation } from "@react-navigation/native";
import { Alert } from "react-native";
import I18n from "../../../../i18n";
import {
AppParamsList,
IOStackNavigationProp
} from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { walletPaymentDeleteTransaction } from "../store/actions/networking";
import { walletPaymentTransactionSelector } from "../store/selectors";
import { WalletPaymentRoutes } from "../navigation/routes";
import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum";

const useWalletPaymentGoBackHandler = () => {
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();
const transactionPot = useIOSelector(walletPaymentTransactionSelector);
const dispatch = useIODispatch();

if (pot.isLoading(transactionPot)) {
// If transaction is pending cancellation we block every interaction with the back button
return () => undefined;
}

// If we have a transaction in the store means that the user has already locked the debt position.
// Before leaving the payment flow we must ask to the user if he is sure he wants to proceed and
// then unlock the debt position by deleting the transaction
if (pot.isSome(transactionPot)) {
const { transactionId } = transactionPot.value;

const handleConfirmAbort = () => {
dispatch(walletPaymentDeleteTransaction.request(transactionId));
navigation.push(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, {
screen: WalletPaymentRoutes.WALLET_PAYMENT_OUTCOME,
params: {
outcome: WalletPaymentOutcomeEnum.CANCELED_BY_USER
}
});
};

return () => {
Alert.alert(I18n.t("wallet.payment.abortDialog.title"), undefined, [
{
text: I18n.t("wallet.payment.abortDialog.confirm"),
style: "destructive",
onPress: handleConfirmAbort
},
{
text: I18n.t("wallet.payment.abortDialog.cancel"),
style: "cancel"
}
]);
};
}

// If there is no transaction stored then we can return undefined (no handler)
return undefined;
};

export { useWalletPaymentGoBackHandler };
8 changes: 8 additions & 0 deletions ts/features/walletV3/payment/saga/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
walletPaymentAuthorization,
walletPaymentCalculateFees,
walletPaymentCreateTransaction,
walletPaymentDeleteTransaction,
walletPaymentGetAllMethods,
walletPaymentGetDetails,
walletPaymentGetUserWallets
Expand All @@ -16,6 +17,7 @@ import { handleWalletPaymentGetAllMethods } from "./networking/handleWalletPayme
import { handleWalletPaymentGetDetails } from "./networking/handleWalletPaymentGetDetails";
import { handleWalletPaymentGetUserWallets } from "./networking/handleWalletPaymentGetUserWallets";
import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization";
import { handleWalletPaymentDeleteTransaction } from "./networking/handleWalletPaymentDeleteTransaction";

/**
* Handle the pagoPA payments requests
Expand Down Expand Up @@ -55,6 +57,12 @@ export function* watchWalletPaymentSaga(
paymentClient.newTransaction
);

yield* takeLatest(
walletPaymentDeleteTransaction.request,
handleWalletPaymentDeleteTransaction,
paymentClient.requestTransactionUserCancellation
);

yield* takeLatest(
walletPaymentAuthorization.request,
handleWalletPaymentAuthorization,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as E from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
import { call, put } from "typed-redux-saga/macro";
import { ActionType } from "typesafe-actions";
import { SagaCallReturnType } from "../../../../../types/utils";
import { getGenericError, getNetworkError } from "../../../../../utils/errors";
import { readablePrivacyReport } from "../../../../../utils/reporters";
import { withRefreshApiCall } from "../../../../fastLogin/saga/utils";
import { PaymentClient } from "../../api/client";
import { walletPaymentDeleteTransaction } from "../../store/actions/networking";

export function* handleWalletPaymentDeleteTransaction(
requestTransactionUserCancellation: PaymentClient["requestTransactionUserCancellation"],
action: ActionType<(typeof walletPaymentDeleteTransaction)["request"]>
) {
const requestTransactionUserCancellationRequest =
requestTransactionUserCancellation({
transactionId: action.payload
});

try {
const requestTransactionUserCancellationResult = (yield* call(
withRefreshApiCall,
requestTransactionUserCancellationRequest,
action
)) as unknown as SagaCallReturnType<
typeof requestTransactionUserCancellation
>;

yield* put(
pipe(
requestTransactionUserCancellationResult,
E.fold(
error =>
walletPaymentDeleteTransaction.failure({
...getGenericError(new Error(readablePrivacyReport(error)))
}),

res => {
if (res.status === 202) {
return walletPaymentDeleteTransaction.success();
}
return walletPaymentDeleteTransaction.failure({
...getGenericError(new Error(`Error: ${res.status}`))
});
}
)
)
);
} catch (e) {
yield* put(
walletPaymentDeleteTransaction.failure({ ...getNetworkError(e) })
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp";
import { WalletPaymentConfirmContent } from "../components/WalletPaymentConfirmContent";
import { useWalletPaymentAuthorizationModal } from "../hooks/useWalletPaymentAuthorizationModal";
import { WalletPaymentRoutes } from "../navigation/routes";
import { walletPaymentCreateTransaction } from "../store/actions/networking";
import {
Expand All @@ -31,6 +30,7 @@ import {
walletPaymentTransactionSelector
} from "../store/selectors";
import { WalletPaymentOutcome } from "../types/PaymentOutcomeEnum";
import { useWalletPaymentAuthorizationModal } from "../hooks/useWalletPaymentAuthorizationModal";

const WalletPaymentConfirmScreen = () => {
const dispatch = useIODispatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard";
import { format } from "../../../../utils/dates";
import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp";
import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet";
import { cleanTransactionDescription } from "../../../../utils/payment";
import {
Expand All @@ -46,7 +47,6 @@ import { WalletPaymentParamsList } from "../navigation/params";
import { WalletPaymentRoutes } from "../navigation/routes";
import { walletPaymentGetDetails } from "../store/actions/networking";
import { walletPaymentDetailsSelector } from "../store/selectors";
import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp";

type WalletPaymentDetailScreenNavigationParams = {
rptId: RptId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { useIOSelector } from "../../../../store/hooks";
import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder";
import { WalletPaymentParamsList } from "../navigation/params";
import { walletPaymentDetailsSelector } from "../store/selectors";
import { WalletPaymentOutcome } from "../types/PaymentOutcomeEnum";
import {
WalletPaymentOutcome,
WalletPaymentOutcomeEnum
} from "../types/PaymentOutcomeEnum";
import { WALLET_PAYMENT_FEEDBACK_URL } from "../utils";

type WalletPaymentOutcomeScreenNavigationParams = {
Expand All @@ -30,7 +33,7 @@ type WalletPaymentOutcomeRouteProps = RouteProp<
>;

const WalletPaymentOutcomeScreen = () => {
const { params: _ } = useRoute<WalletPaymentOutcomeRouteProps>(); // TODO handle outcome (IOBP-437)
const { params } = useRoute<WalletPaymentOutcomeRouteProps>();
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();
const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector);

Expand All @@ -56,6 +59,21 @@ const WalletPaymentOutcomeScreen = () => {

const bannerViewRef = React.useRef<View>(null);

if (params.outcome === WalletPaymentOutcomeEnum.CANCELED_BY_USER) {
return (
<OperationResultScreenContent
pictogram="trash"
title={I18n.t("wallet.payment.outcome.cancelled.title")}
subtitle={I18n.t("wallet.payment.outcome.cancelled.subtitle")}
action={{
label: I18n.t("global.buttons.close"),
accessibilityLabel: I18n.t("global.buttons.close"),
onPress: () => navigation.pop(2)
}}
/>
);
}

return (
<OperationResultScreenContent
pictogram="success"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ import {
walletPaymentAllMethodsSelector,
walletPaymentAmountSelector,
walletPaymentSavedMethodByIdSelector,
walletPaymentTransactionSelector,
walletPaymentUserWalletsSelector
} from "../store/selectors";
import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler";

// ----------------- TYPES -----------------

Expand All @@ -63,6 +65,18 @@ type SelectedMethodState = SavedMethodState | NotSavedMethodState | undefined;
const WalletPaymentPickMethodScreen = () => {
const dispatch = useIODispatch();
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();
const handleGoBack = useWalletPaymentGoBackHandler();

useHeaderSecondLevel({
title: "",
backAccessibilityLabel: I18n.t("global.buttons.back"),
goBack: handleGoBack,
contextualHelp: emptyContextualHelp,
faqCategories: ["payment"],
supportRequest: true
});

const transactionPot = useIOSelector(walletPaymentTransactionSelector);
const getSavedtMethodById = useIOSelector(
walletPaymentSavedMethodByIdSelector
);
Expand All @@ -79,7 +93,9 @@ const WalletPaymentPickMethodScreen = () => {
// walletPaymentGenericMethodByIdSelector
// );
const isLoading =
pot.isLoading(paymentMethodsPot) || pot.isLoading(userWalletsPots);
pot.isLoading(paymentMethodsPot) ||
pot.isLoading(userWalletsPots) ||
pot.isLoading(transactionPot);

const [shouldShowWarningBanner, setShouldShowWarningBanner] =
React.useState<boolean>(false);
Expand Down Expand Up @@ -115,15 +131,6 @@ const WalletPaymentPickMethodScreen = () => {
[paymentMethodsPot, paymentAmount]
);

useHeaderSecondLevel({
title: "",
backAccessibilityLabel: I18n.t("global.buttons.back"),
goBack: navigation.goBack,
contextualHelp: emptyContextualHelp,
faqCategories: ["payment"],
supportRequest: true
});

// ------------------------ HANDLERS --------------------------

const handleSelectSavedMethod = (walletId: string) => {
Expand Down
Loading

0 comments on commit 2986e5c

Please sign in to comment.