Skip to content

Commit

Permalink
chore: [IOBP-503] Add payment attempts tracking (#5401)
Browse files Browse the repository at this point in the history
## Short description
This PR implements the logic to track payment attempts by an user.

## List of changes proposed in this pull request
- Added `attempt` to `PaymentHistory`
- Added attempt store actions into the payment flow.

## How to test
With the `io-dev-api-server`, start a new payment flow within the wallet
playground. Check with reactotron that the store is correctly updated.
Then, restart the app and check that the data is persisted between app
launches.

---------

Co-authored-by: Martino Cesari Tomba <[email protected]>
  • Loading branch information
mastro993 and forrest57 authored Feb 23, 2024
1 parent d09e9b0 commit 54f967a
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 26 deletions.
26 changes: 23 additions & 3 deletions ts/boot/configureStoreAndPersistor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as pot from "@pagopa/ts-commons/lib/pot";
import * as O from "fp-ts/lib/Option";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as O from "fp-ts/lib/Option";
import _, { merge } from "lodash";
import {
applyMiddleware,
Expand All @@ -23,11 +23,13 @@ import {
} from "redux-persist";
import createSagaMiddleware from "redux-saga";
import { remoteUndefined } from "../common/model/RemoteValue";
import { FeaturesState } from "../features/common/store/reducers";
import { CURRENT_REDUX_LOLLIPOP_STORE_VERSION } from "../features/lollipop/store";
import {
initialLollipopState,
LollipopState
} from "../features/lollipop/store/reducers/lollipop";
import { PaymentsState as PaymentsFeatureState } from "../features/payments/common/store/reducers";
import rootSaga from "../sagas";
import { Action, StoreEnhancer } from "../store/actions/types";
import { analytics } from "../store/middlewares";
Expand All @@ -38,8 +40,8 @@ import {
import { ContentState } from "../store/reducers/content";
import { entitiesPersistConfig } from "../store/reducers/entities";
import {
InstallationState,
INSTALLATION_INITIAL_STATE
INSTALLATION_INITIAL_STATE,
InstallationState
} from "../store/reducers/installation";
import { NotificationsState } from "../store/reducers/notifications";
import { getInitialState as getInstallationInitialState } from "../store/reducers/notifications/installation";
Expand Down Expand Up @@ -356,6 +358,24 @@ const migrations: MigrationManifest = {
..._.omit(persistedPreferences, "isExperimentalFeaturesEnabled")
}
};
},
// Version 24
// Adds payments history archive persistence
"24": (state: PersistedState) => {
const features: FeaturesState = (state as PersistedGlobalState).features;
const payments: PaymentsFeatureState = features.payments;
return {
...state,
features: {
...features,
payments: {
...payments,
history: {
archive: []
}
}
}
};
}
};

Expand Down
11 changes: 8 additions & 3 deletions ts/features/payments/history/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { ActionType, createStandardAction } from "typesafe-actions";
import { WalletPaymentOutcome } from "../../../payment/types/PaymentOutcomeEnum";
import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId";

export const walletPaymentStoreNewAttempt = createStandardAction(
"WALLET_PAYMENT_STORE_NEW_ATTEMPT"
)<RptId>();

export const walletPaymentHistoryStoreOutcome = createStandardAction(
"WALLET_PAYMENT_HISTORY_STORE_OUTCOME"
)<WalletPaymentOutcome>();

export type WalletPaymentHistoryActions = ActionType<
typeof walletPaymentHistoryStoreOutcome
>;
export type WalletPaymentHistoryActions =
| ActionType<typeof walletPaymentStoreNewAttempt>
| ActionType<typeof walletPaymentHistoryStoreOutcome>;
55 changes: 40 additions & 15 deletions ts/features/payments/history/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";
import { pipe } from "fp-ts/lib/function";
import _ from "lodash";
import { AsyncStorage } from "react-native";
Expand All @@ -16,7 +17,11 @@ import {
import { walletPaymentInitState } from "../../../payment/store/actions/orchestration";
import { WalletPaymentFailure } from "../../../payment/types/WalletPaymentFailure";
import { PaymentHistory } from "../../types";
import { walletPaymentHistoryStoreOutcome } from "../actions";
import {
walletPaymentStoreNewAttempt,
walletPaymentHistoryStoreOutcome
} from "../actions";
import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId";

export type WalletPaymentHistoryState = {
ongoingPayment?: PaymentHistory;
Expand Down Expand Up @@ -44,17 +49,24 @@ const reducer = (
}
};
case getType(walletPaymentGetDetails.request):
return updatePaymentHistory(
state,
{
rptId: action.payload
},
true
);
return {
...state,
ongoingPayment: {
...state.ongoingPayment,
rptId: action.payload,
attempt: getPaymentAttemptByRptId(state, action.payload)
}
};
case getType(walletPaymentGetDetails.success):
return updatePaymentHistory(state, {
verifiedData: action.payload
});
return {
...state,
ongoingPayment: {
...state.ongoingPayment,
verifiedData: action.payload
}
};
case getType(walletPaymentStoreNewAttempt):
return updatePaymentHistory(state, {}, true);
case getType(walletPaymentCreateTransaction.success):
case getType(walletPaymentGetTransactionInfo.success):
return updatePaymentHistory(state, {
Expand All @@ -81,6 +93,17 @@ const reducer = (
return state;
};

const getPaymentAttemptByRptId = (
state: WalletPaymentHistoryState,
rptId: RptId
) =>
pipe(
state.archive as Array<PaymentHistory>,
A.findFirst(h => h.rptId === rptId),
O.chainNullableK(h => h.attempt),
O.getOrElse(() => 0)
);

const appendItemToArchive = (
archive: ReadonlyArray<PaymentHistory>,
item: PaymentHistory
Expand All @@ -98,14 +121,16 @@ const appendItemToArchive = (
const updatePaymentHistory = (
state: WalletPaymentHistoryState,
data: PaymentHistory,
reset: boolean = false
newAttempt: boolean = false
): WalletPaymentHistoryState => {
const updatedOngoingPaymentHistory = {
const currentAttempt = state.ongoingPayment?.attempt || 0;
const updatedOngoingPaymentHistory: PaymentHistory = {
...state.ongoingPayment,
...data
...data,
attempt: newAttempt ? currentAttempt + 1 : currentAttempt
};

if (reset) {
if (newAttempt) {
return {
ongoingPayment: updatedOngoingPaymentHistory,
archive: appendItemToArchive(state.archive, updatedOngoingPaymentHistory)
Expand Down
23 changes: 23 additions & 0 deletions ts/features/payments/history/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { createSelector } from "reselect";
import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId";
import { GlobalState } from "../../../../../store/reducers/types";

export const selectWalletPaymentHistoryArchive = (state: GlobalState) =>
state.features.payments.history.archive;

export const selectWalletOngoingPaymentHistory = (state: GlobalState) =>
state.features.payments.history.ongoingPayment;

export const walletPaymentAttemptByRptSelector = (rptId: RptId) =>
createSelector(selectWalletPaymentHistoryArchive, archive =>
pipe(
O.fromNullable(archive.find(h => h.rptId === rptId)),
O.chainNullableK(h => h.attempt),
O.getOrElse(() => 0)
)
);

export const walletOngoingPaymentAttemptSelector = createSelector(
selectWalletOngoingPaymentHistory,
paymentHistory =>
pipe(
O.fromNullable(paymentHistory),
O.chainNullableK(h => h.attempt),
O.getOrElse(() => 0)
)
);
1 change: 1 addition & 0 deletions ts/features/payments/history/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type PaymentHistory = {
transaction?: NewTransactionResponse;
outcome?: WalletPaymentOutcomeEnum;
failure?: WalletPaymentFailure;
attempt?: number;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as React from "react";
import URLParse from "url-parse";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { WALLET_WEBVIEW_OUTCOME_SCHEMA } from "../../common/utils/const";
import { walletPaymentHistoryStoreOutcome } from "../../history/store/actions";
import {
WalletPaymentAuthorizePayload,
walletPaymentAuthorization
Expand All @@ -16,7 +17,6 @@ import {
WalletPaymentOutcome,
WalletPaymentOutcomeEnum
} from "../types/PaymentOutcomeEnum";
import { walletPaymentHistoryStoreOutcome } from "../../history/store/actions";

type Props = {
onAuthorizationOutcome: (outcome: WalletPaymentOutcome) => void;
Expand All @@ -35,6 +35,7 @@ export const useWalletPaymentAuthorizationModal = ({
onDismiss
}: Props): WalletPaymentAuthorizationModal => {
const dispatch = useIODispatch();

const authorizationUrlPot = useIOSelector(
walletPaymentAuthorizationUrlSelector
);
Expand All @@ -52,8 +53,8 @@ export const useWalletPaymentAuthorizationModal = ({
WalletPaymentOutcome.decode,
E.getOrElse(() => WalletPaymentOutcomeEnum.GENERIC_ERROR)
);
dispatch(walletPaymentHistoryStoreOutcome(outcome));
onAuthorizationOutcome(outcome);
dispatch(walletPaymentHistoryStoreOutcome(outcome));
},
[onAuthorizationOutcome, dispatch]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { WalletPaymentRoutes } from "../navigation/routes";
import { walletPaymentGetDetails } from "../store/actions/networking";
import { walletPaymentDetailsSelector } from "../store/selectors";
import { WalletPaymentFailure } from "../types/WalletPaymentFailure";
import { walletPaymentStoreNewAttempt } from "../../history/store/actions";

type WalletPaymentDetailScreenNavigationParams = {
rptId: RptId;
Expand Down Expand Up @@ -117,6 +118,7 @@ const WalletPaymentDetailContent = ({
rptId,
payment
}: WalletPaymentDetailContentProps) => {
const dispatch = useIODispatch();
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();

useLayoutEffect(() => {
Expand All @@ -132,6 +134,7 @@ const WalletPaymentDetailContent = ({
});

const navigateToMakePaymentScreen = () => {
dispatch(walletPaymentStoreNewAttempt(rptId));
navigation.push(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, {
screen: WalletPaymentRoutes.WALLET_PAYMENT_MAKE
});
Expand Down
6 changes: 3 additions & 3 deletions ts/features/payments/payment/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/eco
import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse";
import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId";
import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo";
import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo";
import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets";
import { Action } from "../../../../../store/actions/types";
import { NetworkError } from "../../../../../utils/errors";
import { PaymentStartRoute } from "../../types";
import { WalletPaymentFailure } from "../../types/WalletPaymentFailure";
import {
walletPaymentAuthorization,
walletPaymentCalculateFees,
Expand All @@ -32,6 +29,9 @@ import {
walletPaymentResetPickedPsp,
walletPaymentSetCurrentStep
} from "../actions/orchestration";
import { WalletPaymentFailure } from "../../types/WalletPaymentFailure";
import { Wallets } from "../../../../../../definitions/pagopa/ecommerce/Wallets";
import { WalletInfo } from "../../../../../../definitions/pagopa/ecommerce/WalletInfo";

export const WALLET_PAYMENT_STEP_MAX = 4;

Expand Down

0 comments on commit 54f967a

Please sign in to comment.