Skip to content

Commit

Permalink
feat: [IOBP-687,IOBP-885] Add recently used payment method from the p…
Browse files Browse the repository at this point in the history
…ayment flow (#6234)

## Short description
This PR implements the ability to display the most recently used payment
method during in-app payments, ensuring it matches the method used in a
previous transaction.

## List of changes proposed in this pull request
- Updated the open API definitions;
- Added saga, action & reducer about the `recentUsedPaymentMethod`
- Added an additional list item at the top of the list that contains the
most recently used payment method if available, otherwise it isn't
showed
- Added a logic to remove the recently used payment method from the
cluster membership

## How to test
- Checkout this PR: pagopa/io-dev-api-server#415
from the io-dev-api-server and generate the API definitions;
- Start the io-dev-api-server;
- With the app in local env, start a payment flow from the "Payments"
section screen tapping the CTA "Paga un avviso"
- Then tap on the CTA "Digita"
- Inside of it, type any notice code -> go forward and type any fiscal
code;
- After, you should be able to see a list of available payment method,
complete the first payment choosing any payment method;
- When you complete the transaction, try to start another payment flow,
you should be able to see the payment method previously you choose in
the "Recently used" section;

## Preview


https://github.com/user-attachments/assets/62c30602-9581-42cd-b75c-dd3c0703aa22

---------

Co-authored-by: Mario Perrotta <[email protected]>
  • Loading branch information
Hantex9 and hevelius authored Oct 14, 2024
1 parent 7fe89b8 commit a081ed4
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 41 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"services_api": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_services_app_backend.yaml",
"lollipop_api": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_lollipop_first_consumer.yaml",
"fast_login_api": "https://raw.githubusercontent.com/pagopa/io-auth-n-identity-domain/[email protected]/apps/io-session-manager/api/fast-login.yaml",
"pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.162.2/src/domains/pay-wallet-app/api/io-payment-wallet/v1/_openapi.json.tpl",
"pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.162.2/src/domains/ecommerce-app/api/ecommerce-io/v2/_openapi.json.tpl",
"pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.202.0/src/domains/pay-wallet-app/api/io-payment-wallet/v1/_openapi.json.tpl",
"pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.202.0/src/domains/ecommerce-app/api/ecommerce-io/v2/_openapi.json.tpl",
"pagopa_api_biz_events": "https://raw.githubusercontent.com/pagopa/pagopa-biz-events-service/0.1.37/openapi/openapi_io_patch.json",
"pagopa_api_platform": "https://raw.githubusercontent.com/pagopa/pagopa-infra/v1.64.0/src/domains/shared-app/api/session-wallet/v1/_openapi.json.tpl",
"trial_system": "https://raw.githubusercontent.com/pagopa/io-backend/v14.3.0-RELEASE/api_trial_system.yaml",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
walletPaymentAllMethodsSelector,
walletPaymentEnabledUserWalletsSelector,
walletPaymentSelectedPaymentMethodIdOptionSelector,
walletPaymentSelectedWalletIdOptionSelector
walletPaymentSelectedWalletIdOptionSelector,
walletRecentPaymentMethodSelector
} from "../store/selectors/paymentMethods";
import { getPaymentLogoFromWalletDetails } from "../../common/utils";
import { WalletStatusEnum } from "../../../../../definitions/pagopa/ecommerce/WalletStatus";

const CheckoutPaymentMethodsList = () => {
const dispatch = useIODispatch();
Expand All @@ -35,6 +37,9 @@ const CheckoutPaymentMethodsList = () => {
const paymentAmountPot = useIOSelector(walletPaymentAmountSelector);
const allPaymentMethods = useIOSelector(walletPaymentAllMethodsSelector);
const userWallets = useIOSelector(walletPaymentEnabledUserWalletsSelector);
const recentUsedPaymentMethod = useIOSelector(
walletRecentPaymentMethodSelector
);

const selectedUserWalletIdOption = useIOSelector(
walletPaymentSelectedWalletIdOptionSelector
Expand All @@ -49,6 +54,23 @@ const CheckoutPaymentMethodsList = () => {
O.getOrElse(() => 0)
);

const recentPaymentMethodListItem = useMemo(
() =>
pipe(
recentUsedPaymentMethod,
O.fromNullable,
O.chainNullableK(a => {
if (a.status === WalletStatusEnum.VALIDATED) {
return mapUserWalletToRadioItem(a);
}
return mapPaymentMethodToRadioItem(a, paymentAmount);
}),
O.map(A.of),
O.getOrElse(() => [] as Array<RadioItem<string>>)
),
[recentUsedPaymentMethod, paymentAmount]
);

const userPaymentMethodListItems = useMemo(
() =>
pipe(
Expand All @@ -57,9 +79,15 @@ const CheckoutPaymentMethodsList = () => {
O.map(methods => methods.map(mapUserWalletToRadioItem)),
O.map(A.map(O.fromNullable)),
O.map(A.compact),
O.map(
A.filter(
method =>
!recentPaymentMethodListItem.some(item => item.id === method.id)
)
),
O.getOrElse(() => [] as Array<RadioItem<string>>)
),
[userWallets]
[userWallets, recentPaymentMethodListItem]
);

const allPaymentMethodListItems = useMemo(
Expand All @@ -70,18 +98,30 @@ const CheckoutPaymentMethodsList = () => {
O.map(methods =>
methods.map(item => mapPaymentMethodToRadioItem(item, paymentAmount))
),
O.map(
A.filter(
method =>
!recentPaymentMethodListItem.some(item => item.id === method.id)
)
),
O.getOrElse(() => [] as Array<RadioItem<string>>)
),
[allPaymentMethods, paymentAmount]
[allPaymentMethods, paymentAmount, recentPaymentMethodListItem]
);

useEffect(() => {
const hasDisabledMethods =
[...userPaymentMethodListItems, ...allPaymentMethodListItems].find(
item => item.disabled
) !== undefined;
[
...userPaymentMethodListItems,
...allPaymentMethodListItems,
...recentPaymentMethodListItem
].find(item => item.disabled) !== undefined;
setShouldShowWarningBanner(hasDisabledMethods);
}, [userPaymentMethodListItems, allPaymentMethodListItems]);
}, [
userPaymentMethodListItems,
allPaymentMethodListItems,
recentPaymentMethodListItem
]);

const handleSelectUserWallet = (walletId: string) =>
pipe(
Expand Down Expand Up @@ -115,6 +155,11 @@ const CheckoutPaymentMethodsList = () => {
})
);

const handleOnSelectRecentPaymentMethod = (walletId: string) =>
recentUsedPaymentMethod?.status === WalletStatusEnum.VALIDATED
? handleSelectUserWallet(walletId)
: handleSelectPaymentMethod(walletId);

const selectedWalletId = O.toUndefined(selectedUserWalletIdOption);
const selectedPaymentMethodId = O.toUndefined(selectedPaymentMethodIdOption);

Expand All @@ -128,6 +173,17 @@ const CheckoutPaymentMethodsList = () => {
action={I18n.t("wallet.payment.methodSelection.alert.cta")}
/>
)}
{!_.isEmpty(recentPaymentMethodListItem) && (
<ListItemHeader
label={I18n.t("wallet.payment.methodSelection.latestMethod")}
/>
)}
<RadioGroup<string>
type="radioListItem"
selectedItem={selectedWalletId || selectedPaymentMethodId}
items={recentPaymentMethodListItem}
onPress={handleOnSelectRecentPaymentMethod}
/>
{!_.isEmpty(userPaymentMethodListItems) && (
<ListItemHeader
label={I18n.t("wallet.payment.methodSelection.yourMethods")}
Expand Down
8 changes: 8 additions & 0 deletions ts/features/payments/checkout/saga/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
paymentsGetPaymentMethodsAction,
paymentsGetPaymentTransactionInfoAction,
paymentsGetPaymentUserMethodsAction,
paymentsGetRecentPaymentMethodUsedAction,
paymentsStartPaymentAuthorizationAction
} from "../store/actions/networking";
import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization";
Expand All @@ -19,6 +20,7 @@ import { handleWalletPaymentGetAllMethods } from "./networking/handleWalletPayme
import { handleWalletPaymentGetDetails } from "./networking/handleWalletPaymentGetDetails";
import { handleWalletPaymentGetTransactionInfo } from "./networking/handleWalletPaymentGetTransactionInfo";
import { handleWalletPaymentGetUserWallets } from "./networking/handleWalletPaymentGetUserWallets";
import { handleWalletPaymentGetRecentMethod } from "./networking/handleWalletPaymentGetRecentMethod";

/**
* Handle the pagoPA payments requests
Expand Down Expand Up @@ -74,4 +76,10 @@ export function* watchPaymentsCheckoutSaga(
handleWalletPaymentAuthorization,
paymentClient.requestTransactionAuthorizationForIO
);

yield* takeLatest(
paymentsGetRecentPaymentMethodUsedAction.request,
handleWalletPaymentGetRecentMethod,
paymentClient.getUserLastPaymentMethodUsed
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as E from "fp-ts/lib/Either";
import { put } from "typed-redux-saga/macro";
import { ActionType } from "typesafe-actions";
import { getGenericError, getNetworkError } from "../../../../../utils/errors";
import { readablePrivacyReport } from "../../../../../utils/reporters";
import { PaymentClient } from "../../../common/api/client";
import { paymentsGetRecentPaymentMethodUsedAction } from "../../store/actions/networking";
import { withPaymentsSessionToken } from "../../../common/utils/withPaymentsSessionToken";

export function* handleWalletPaymentGetRecentMethod(
getUserLastPaymentMethod: PaymentClient["getUserLastPaymentMethodUsed"],
action: ActionType<
(typeof paymentsGetRecentPaymentMethodUsedAction)["request"]
>
) {
try {
const getAllPaymentMethodsResult = yield* withPaymentsSessionToken(
getUserLastPaymentMethod,
action,
{},
"pagoPAPlatformSessionToken"
);

if (E.isLeft(getAllPaymentMethodsResult)) {
yield* put(
paymentsGetRecentPaymentMethodUsedAction.failure({
...getGenericError(
new Error(readablePrivacyReport(getAllPaymentMethodsResult.left))
)
})
);
return;
}
const res = getAllPaymentMethodsResult.right;
if (res.status === 200) {
yield* put(paymentsGetRecentPaymentMethodUsedAction.success(res.value));
} else if (res.status !== 401) {
// The 401 status is handled by the withPaymentsSessionToken
yield* put(
paymentsGetRecentPaymentMethodUsedAction.failure({
...getGenericError(new Error(`Error: ${res.status}`))
})
);
}
} catch (e) {
yield* put(
paymentsGetRecentPaymentMethodUsedAction.failure({
...getNetworkError(e)
})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { PaymentsCheckoutRoutes } from "../navigation/routes";
import {
paymentsCalculatePaymentFeesAction,
paymentsCreateTransactionAction,
paymentsGetPaymentMethodsAction
paymentsGetPaymentMethodsAction,
paymentsGetRecentPaymentMethodUsedAction
} from "../store/actions/networking";
import {
walletPaymentAmountSelector,
Expand Down Expand Up @@ -75,6 +76,10 @@ const WalletPaymentPickMethodScreen = () => {
}, [dispatch])
);

useOnFirstRender(() => {
dispatch(paymentsGetRecentPaymentMethodUsedAction.request());
});

const calculateFeesForSelectedPaymentMethod = React.useCallback(() => {
pipe(
sequenceT(O.Monad)(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const INITIAL_STATE: PaymentsCheckoutState = {
paymentDetails: pot.none,
userWallets: pot.none,
allPaymentMethods: pot.none,
recentUsedPaymentMethod: pot.none,
pspList: pot.none,
selectedWallet: O.none,
selectedPaymentMethod: O.none,
Expand Down
8 changes: 8 additions & 0 deletions ts/features/payments/checkout/store/actions/networking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets"
import { NetworkError } from "../../../../../utils/errors";
import { WalletPaymentFailure } from "../../types/WalletPaymentFailure";
import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo";
import { UserLastPaymentMethodResponse } from "../../../../../../definitions/pagopa/ecommerce/UserLastPaymentMethodResponse";

export const paymentsGetPaymentDetailsAction = createAsyncAction(
"PAYMENTS_GET_PAYMENT_DETAILS_REQUEST",
Expand All @@ -37,6 +38,12 @@ export const paymentsGetPaymentUserMethodsAction = createAsyncAction(
"PAYMENTS_GET_PAYMENT_USER_METHODS_FAILURE"
)<PaymentGetPaymentUserMethodsPayload, Wallets, NetworkError>();

export const paymentsGetRecentPaymentMethodUsedAction = createAsyncAction(
"PAYMENTS_GET_RECENT_PAYMENT_METHOD_REQUEST",
"PAYMENTS_GET_RECENT_PAYMENT_METHOD_SUCCESS",
"PAYMENTS_GET_RECENT_PAYMENT_METHOD_FAILURE"
)<undefined, UserLastPaymentMethodResponse, NetworkError>();

export type CalculateFeePayload = {
paymentMethodId: string;
idPsp?: string;
Expand Down Expand Up @@ -110,4 +117,5 @@ export type PaymentsCheckoutNetworkingActions =
| ActionType<typeof paymentsCreateTransactionAction>
| ActionType<typeof paymentsGetPaymentTransactionInfoAction>
| ActionType<typeof paymentsDeleteTransactionAction>
| ActionType<typeof paymentsGetRecentPaymentMethodUsedAction>
| ActionType<typeof paymentsStartPaymentAuthorizationAction>;
27 changes: 27 additions & 0 deletions ts/features/payments/checkout/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
paymentsGetPaymentMethodsAction,
paymentsGetPaymentTransactionInfoAction,
paymentsGetPaymentUserMethodsAction,
paymentsGetRecentPaymentMethodUsedAction,
paymentsStartPaymentAuthorizationAction
} from "../actions/networking";
import {
Expand All @@ -33,6 +34,7 @@ import {
selectPaymentPspAction,
walletPaymentSetCurrentStep
} from "../actions/orchestration";
import { UserLastPaymentMethodResponse } from "../../../../../../definitions/pagopa/ecommerce/UserLastPaymentMethodResponse";
export const WALLET_PAYMENT_STEP_MAX = 4;

export type PaymentsCheckoutState = {
Expand All @@ -43,6 +45,7 @@ export type PaymentsCheckoutState = {
NetworkError | WalletPaymentFailure
>;
userWallets: pot.Pot<Wallets, NetworkError>;
recentUsedPaymentMethod: pot.Pot<UserLastPaymentMethodResponse, NetworkError>;
allPaymentMethods: pot.Pot<PaymentMethodsResponse, NetworkError>;
pspList: pot.Pot<ReadonlyArray<Bundle>, NetworkError>;
selectedWallet: O.Option<WalletInfo>;
Expand All @@ -57,6 +60,7 @@ const INITIAL_STATE: PaymentsCheckoutState = {
currentStep: WalletPaymentStepEnum.PICK_PAYMENT_METHOD,
paymentDetails: pot.none,
userWallets: pot.none,
recentUsedPaymentMethod: pot.none,
allPaymentMethods: pot.none,
pspList: pot.none,
selectedWallet: O.none,
Expand Down Expand Up @@ -89,6 +93,9 @@ const reducer = (
return {
...state,
rptId: action.payload,
recentUsedPaymentMethod: pot.none,
selectedPaymentMethod: O.none,
selectedWallet: O.none,
paymentDetails: pot.toLoading(state.paymentDetails)
};
case getType(paymentsGetPaymentDetailsAction.success):
Expand Down Expand Up @@ -136,6 +143,26 @@ const reducer = (
allPaymentMethods: pot.toError(state.allPaymentMethods, action.payload)
};

// Recent payment method
case getType(paymentsGetRecentPaymentMethodUsedAction.request):
return {
...state,
recentUsedPaymentMethod: pot.toLoading(state.recentUsedPaymentMethod)
};
case getType(paymentsGetRecentPaymentMethodUsedAction.success):
return {
...state,
recentUsedPaymentMethod: pot.some(action.payload)
};
case getType(paymentsGetRecentPaymentMethodUsedAction.failure):
return {
...state,
recentUsedPaymentMethod: pot.toError(
state.recentUsedPaymentMethod,
action.payload
)
};

case getType(selectPaymentMethodAction):
return {
...state,
Expand Down
Loading

0 comments on commit a081ed4

Please sign in to comment.