Skip to content

Commit

Permalink
chore: [IOBP-521] Add transaction status polling before PSP selection…
Browse files Browse the repository at this point in the history
… screen (#5443)

Depends on #5439

## Short description
This PR adds the polling for transaction activation in the payment flow

## List of changes proposed in this pull request
- Add `useOnTransactionActivationEffect` custom hook which handles the
polling for the transaction status
- Added status polling before navigate to the PSP selection screen

## How to test
With the `io-dev-api-server`, checkout [this
branch](pagopa/io-dev-api-server#342), from
**Profile > Playground > New Wallet > Payment** start a payment flow and
check that the payment can be finalized.
  • Loading branch information
mastro993 authored Jan 26, 2024
1 parent 18f8cb3 commit 0a7da49
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react";
import { TransactionInfo } from "../../../../../definitions/pagopa/ecommerce/TransactionInfo";
import { TransactionStatusEnum } from "../../../../../definitions/pagopa/ecommerce/TransactionStatus";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { getGenericError } from "../../../../utils/errors";
import { walletPaymentGetTransactionInfo } from "../store/actions/networking";
import { walletPaymentTransactionSelector } from "../store/selectors";

const INITIAL_DELAY = 250;
const MAX_TRIES = 3;

type EffectCallback = (
transaction: TransactionInfo
) => void | (() => void | undefined);

/**
* This custom hook manages the transition of a transaction's status from ACTIVATION_REQUESTED to ACTIVATED.
* It employs a polling mechanism to continuously check the status, and once the status becomes ACTIVATED,
* the specified effect is triggered.
* @param effect Function to be executed upon transaction activation
*/
const useOnTransactionActivationEffect = (effect: EffectCallback) => {
const dispatch = useIODispatch();
const transactionPot = useIOSelector(walletPaymentTransactionSelector);

const delayRef = React.useRef(INITIAL_DELAY);
const countRef = React.useRef(0);

/* eslint-disable functional/immutable-data */
React.useEffect(() => {
if (transactionPot.kind === "PotSome") {
const { transactionId, status } = transactionPot.value;

if (status === TransactionStatusEnum.ACTIVATED) {
// Execute the effect function when the transaction is activated
delayRef.current = INITIAL_DELAY;
countRef.current = 0;
return effect(transactionPot.value);
} else if (countRef.current > MAX_TRIES) {
// The transaction is not yet ACTIVATED, and we exceeded the max retries
dispatch(
walletPaymentGetTransactionInfo.failure(
getGenericError(new Error("Max try reached"))
)
);
return;
} else {
// The transaction is not yet ACTIVATED, continue polling for transaction status with a timeout
const timeout = setTimeout(() => {
delayRef.current *= 2;
countRef.current += 1;
dispatch(walletPaymentGetTransactionInfo.request({ transactionId }));
}, delayRef.current);
// Clean up the timeout to avoid memory leaks
return () => {
clearTimeout(timeout);
};
}
}

return undefined;
}, [dispatch, transactionPot, effect]);
};

export { useOnTransactionActivationEffect };
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 @@ -9,6 +9,7 @@ import {
walletPaymentDeleteTransaction,
walletPaymentGetAllMethods,
walletPaymentGetDetails,
walletPaymentGetTransactionInfo,
walletPaymentGetUserWallets,
walletPaymentNewSessionToken
} from "../store/actions/networking";
Expand All @@ -20,6 +21,7 @@ import { handleWalletPaymentGetUserWallets } from "./networking/handleWalletPaym
import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization";
import { handleWalletPaymentDeleteTransaction } from "./networking/handleWalletPaymentDeleteTransaction";
import { handleWalletPaymentNewSessionToken } from "./networking/handleWalletPaymentNewSessionToken";
import { handleWalletPaymentGetTransactionInfo } from "./networking/handleWalletPaymentGetTransactionInfo";

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

yield* takeLatest(
walletPaymentGetTransactionInfo.request,
handleWalletPaymentGetTransactionInfo,
paymentClient.getTransactionInfo
);

yield* takeLatest(
walletPaymentDeleteTransaction.request,
handleWalletPaymentDeleteTransaction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,27 @@ export function* handleWalletPaymentCreateTransaction(
return;
}

const calculateFeesRequest = newTransaction({
const newTransactionRequest = newTransaction({
body: action.payload,
eCommerceSessionToken: sessionToken
});

const calculateFeesResult = (yield* call(
const newTransactionResult = (yield* call(
withRefreshApiCall,
calculateFeesRequest,
newTransactionRequest,
action
)) as SagaCallReturnType<typeof newTransaction>;

yield* put(
pipe(
calculateFeesResult,
newTransactionResult,
E.fold(
error =>
walletPaymentCreateTransaction.failure({
...getGenericError(new Error(readablePrivacyReport(error)))
}),
({ status, value }) => {
if (status === 200) {
action.payload.onSucces?.();
return walletPaymentCreateTransaction.success(value);
} else if (status === 400) {
return walletPaymentCreateTransaction.failure({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 { walletPaymentGetTransactionInfo } from "../../store/actions/networking";
import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken";

export function* handleWalletPaymentGetTransactionInfo(
getTransactionInfo: PaymentClient["getTransactionInfo"],
action: ActionType<(typeof walletPaymentGetTransactionInfo)["request"]>
) {
const sessionToken = yield* getOrFetchWalletSessionToken();

if (sessionToken === undefined) {
yield* put(
walletPaymentGetTransactionInfo.failure({
...getGenericError(new Error(`Missing session token`))
})
);
return;
}

const getTransactionInfoRequest = getTransactionInfo({
eCommerceSessionToken: sessionToken,
transactionId: action.payload.transactionId
});

try {
const getTransactionInfoResult = (yield* call(
withRefreshApiCall,
getTransactionInfoRequest,
action
)) as SagaCallReturnType<typeof getTransactionInfo>;

yield* put(
pipe(
getTransactionInfoResult,
E.fold(
error =>
walletPaymentGetTransactionInfo.failure({
...getGenericError(new Error(readablePrivacyReport(error)))
}),
({ status, value }) => {
if (status === 200) {
return walletPaymentGetTransactionInfo.success(value);
} else {
return walletPaymentGetTransactionInfo.failure({
...getGenericError(new Error(JSON.stringify(value)))
});
}
}
)
)
);
} catch (e) {
yield* put(
walletPaymentGetTransactionInfo.failure({ ...getNetworkError(e) })
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const WalletPaymentConfirmScreen = () => {
startPaymentAuthorizaton({
paymentAmount: paymentDetail.amount as AmountEuroCents,
paymentFees: (selectedPsp.taxPayerFee ?? 0) as AmountEuroCents,
pspId: selectedPsp.idBundle ?? "",
pspId: selectedPsp.idPsp ?? "",
transactionId: transaction.transactionId,
walletId: selectedMethod.walletId
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { findFirstCaseInsensitive } from "../../../../utils/object";
import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails";
import { WalletPaymentMissingMethodsError } from "../components/WalletPaymentMissingMethodsError";
import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler";
import { useOnTransactionActivationEffect } from "../hooks/useOnTransactionActivationEffect";
import { WalletPaymentRoutes } from "../navigation/routes";
import {
walletPaymentCreateTransaction,
Expand Down Expand Up @@ -78,20 +79,35 @@ const WalletPaymentPickMethodScreen = () => {
});

const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector);
const transactionPot = useIOSelector(walletPaymentTransactionSelector);
const getSavedtMethodById = useIOSelector(
walletPaymentSavedMethodByIdSelector
);
const paymentAmountPot = useIOSelector(walletPaymentAmountSelector);
const paymentMethodsPot = useIOSelector(walletPaymentAllMethodsSelector);
const userWalletsPots = useIOSelector(walletPaymentUserWalletsSelector);
const transactionPot = useIOSelector(walletPaymentTransactionSelector);
// const getGenericMethodById = useIOSelector(walletPaymentGenericMethodByIdSelector);

const [waitingTransactionActivation, setWaitingTransactionActivation] =
React.useState(false);

// When a new transaction is created it comes with ACTIVATION_REQUESTED status, we can continue the payment flow
// only when the transaction status becomes ACTIVATED.
useOnTransactionActivationEffect(
React.useCallback(() => {
navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, {
screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP
});
setWaitingTransactionActivation(false);
}, [navigation])
);

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

const isLoading =
pot.isLoading(paymentMethodsPot) || pot.isLoading(userWalletsPots);
const isLoadingTransaction = pot.isLoading(transactionPot);
const isLoadingTransaction =
pot.isLoading(transactionPot) || waitingTransactionActivation;

const isError =
pot.isError(transactionPot) ||
Expand All @@ -105,7 +121,8 @@ const WalletPaymentPickMethodScreen = () => {

useFocusEffect(
React.useCallback(() => {
// dispatch(walletPaymentGetAllMethods.request()); // currently we do not allow onboarding new methods in payment flow
// currently we do not allow onboarding new methods in payment flow
// dispatch(walletPaymentGetAllMethods.request());
dispatch(walletPaymentGetUserWallets.request());
}, [dispatch])
);
Expand Down Expand Up @@ -157,21 +174,6 @@ const WalletPaymentPickMethodScreen = () => {
});
};

/* Will be decommented once generic methods are implemented
const handleSelectNotSavedMethod = (methodId: string) => {
setSelectedMethod({
kind: "generic",
methodId
});
};
*/

const navigateToPspSelectionScreen = () => {
navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, {
screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP
});
};

const handleContinue = () => {
// todo:: should handle the case where the user
// selects a non saved method
Expand All @@ -187,10 +189,10 @@ const WalletPaymentPickMethodScreen = () => {
walletPaymentCreateTransaction.request({
paymentNotices: [
{ rptId: paymentDetails.rptId, amount: paymentDetails.amount }
],
onSucces: navigateToPspSelectionScreen
]
})
);
setWaitingTransactionActivation(true);
})
);
}
Expand Down
25 changes: 13 additions & 12 deletions ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import React from "react";
import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle";
import { Transfer } from "../../../../../definitions/pagopa/ecommerce/Transfer";
import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
import I18n from "../../../../i18n";
import {
Expand All @@ -37,7 +38,7 @@ import {
walletPaymentPickedPaymentMethodSelector,
walletPaymentPickedPspSelector,
walletPaymentPspListSelector,
walletPaymentTransactionTransferListSelector
walletPaymentTransactionSelector
} from "../store/selectors";
import { WalletPaymentPspSortType } from "../types";
import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum";
Expand All @@ -53,9 +54,7 @@ const WalletPaymentPickPspScreen = () => {
const [sortType, setSortType] =
React.useState<WalletPaymentPspSortType>("default");

const transactionTransferListPot = useIOSelector(
walletPaymentTransactionTransferListSelector
);
const transactionPot = useIOSelector(walletPaymentTransactionSelector);
const pspListPot = useIOSelector(walletPaymentPspListSelector);
const selectedPspOption = useIOSelector(walletPaymentPickedPspSelector);

Expand Down Expand Up @@ -93,12 +92,19 @@ const WalletPaymentPickPspScreen = () => {
pipe(
sequenceT(O.Monad)(
pot.toOption(paymentAmountPot),
pot.toOption(transactionTransferListPot),
pot.toOption(transactionPot),
selectedWalletOption
),
O.map(([paymentAmountInCents, transferList, selectedWallet]) => {
O.map(([paymentAmountInCents, transaction, selectedWallet]) => {
const transferList = transaction.payments.reduce(
(a, p) => [...a, ...(p.transferList ?? [])],
[] as ReadonlyArray<Transfer>
);
const paymentToken = transaction.payments[0]?.paymentToken;

dispatch(
walletPaymentCalculateFees.request({
paymentToken,
paymentMethodId: selectedWallet.paymentMethodId,
walletId: selectedWallet.walletId,
paymentAmount: paymentAmountInCents,
Expand All @@ -107,12 +113,7 @@ const WalletPaymentPickPspScreen = () => {
);
})
);
}, [
dispatch,
paymentAmountPot,
selectedWalletOption,
transactionTransferListPot
])
}, [dispatch, paymentAmountPot, selectedWalletOption, transactionPot])
);

React.useEffect(
Expand Down
10 changes: 9 additions & 1 deletion ts/features/walletV3/payment/store/actions/networking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets";
import { NetworkError } from "../../../../../utils/errors";
import { WalletPaymentFailure } from "../../types/failure";
import { NewSessionTokenResponse } from "../../../../../../definitions/pagopa/ecommerce/NewSessionTokenResponse";
import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo";
import { CalculateFeeRequest } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeRequest";

export const walletPaymentNewSessionToken = createAsyncAction(
Expand Down Expand Up @@ -52,11 +53,17 @@ export const walletPaymentCreateTransaction = createAsyncAction(
"WALLET_PAYMENT_CREATE_TRANSACTION_SUCCESS",
"WALLET_PAYMENT_CREATE_TRANSACTION_FAILURE"
)<
NewTransactionRequest & { onSucces?: () => void },
NewTransactionRequest,
NewTransactionResponse,
NetworkError | WalletPaymentFailure
>();

export const walletPaymentGetTransactionInfo = createAsyncAction(
"WALLET_PAYMENT_GET_TRANSACTION_INFO_REQUEST",
"WALLET_PAYMENT_GET_TRANSACTION_INFO_SUCCESS",
"WALLET_PAYMENT_GET_TRANSACTION_INFO_FAILURE"
)<{ transactionId: string }, TransactionInfo, NetworkError>();

export const walletPaymentDeleteTransaction = createAsyncAction(
"WALLET_PAYMENT_DELETE_TRANSACTION_REQUEST",
"WALLET_PAYMENT_DELETE_TRANSACTION_SUCCESS",
Expand Down Expand Up @@ -90,5 +97,6 @@ export type WalletPaymentNetworkingActions =
| ActionType<typeof walletPaymentGetUserWallets>
| ActionType<typeof walletPaymentCalculateFees>
| ActionType<typeof walletPaymentCreateTransaction>
| ActionType<typeof walletPaymentGetTransactionInfo>
| ActionType<typeof walletPaymentDeleteTransaction>
| ActionType<typeof walletPaymentAuthorization>;
Loading

0 comments on commit 0a7da49

Please sign in to comment.