Skip to content

Commit

Permalink
Merge branch 'master' into IOBP-833-GEC-sorting-logic
Browse files Browse the repository at this point in the history
  • Loading branch information
LeleDallas authored Dec 23, 2024
2 parents d9087b3 + f739c10 commit 9d11d1f
Show file tree
Hide file tree
Showing 24 changed files with 570 additions and 221 deletions.
2 changes: 2 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3240,6 +3240,8 @@ features:
empty:
title: Nessuna ricevuta trovata
subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore.
emptyPayer:
title: Qui vedrai le ricevute dei pagamenti fatti in app
details:
payPal:
banner:
Expand Down
2 changes: 2 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3240,6 +3240,8 @@ features:
empty:
title: Nessuna ricevuta trovata
subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore.
emptyPayer:
title: Qui vedrai le ricevute dei pagamenti fatti in app
details:
payPal:
banner:
Expand Down
17 changes: 17 additions & 0 deletions ts/components/debug/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { truncateObjectStrings } from "../utils";

describe("truncateObjectStrings", () => {
it.each`
input | maxLength | expected
${"Long string"} | ${4} | ${"Long..."}
${{ outer: { inner: "Long string" }, bool: true }} | ${4} | ${{ outer: { inner: "Long..." }, bool: true }}
${["Long string", "Very long string"]} | ${4} | ${["Long...", "Very..."]}
${new Set(["Long string", "Very long string"])} | ${4} | ${["Long...", "Very..."]}
`(
"$input should be truncated to $expected",
({ input, maxLength, expected }) => {
const result = truncateObjectStrings(input, maxLength);
expect(result).toEqual(expected);
}
);
});
15 changes: 14 additions & 1 deletion ts/components/debug/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
type Primitive = string | number | boolean | null | undefined;

type TruncatableValue = Primitive | TruncatableObject | TruncatableArray;
type TruncatableValue =
| Primitive
| TruncatableObject
| TruncatableArray
| TruncatableSet;

interface TruncatableObject {
[key: string]: TruncatableValue;
}

type TruncatableArray = Array<TruncatableValue>;
type TruncatableSet = Set<TruncatableValue>;

/**
* Truncates all string values in an object or array structure to a specified maximum length.
Expand Down Expand Up @@ -37,6 +42,14 @@ export const truncateObjectStrings = <T extends TruncatableValue>(
}

if (typeof value === "object" && value !== null) {
if (value instanceof Set) {
// Set could not be serialized to JSON because values are not stored as properties
// For display purposes, we convert it to an array
return Array.from(value).map(item =>
truncateObjectStrings(item, maxLength)
) as T;
}

return Object.entries(value).reduce(
(acc, [key, val]) => ({
...acc,
Expand Down
6 changes: 4 additions & 2 deletions ts/components/debug/withDebugEnabled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { isDebugModeEnabledSelector } from "../../store/reducers/debug";
* This HOC allows to render the wrapped component only if the debug mode is enabled, otherwise returns null (nothing)
*/
export const withDebugEnabled =
<P,>(WrappedComponent: React.ComponentType<P>) =>
<P extends Record<string, unknown>>(
WrappedComponent: React.ComponentType<P>
) =>
(props: P) => {
const isDebug = useIOSelector(isDebugModeEnabledSelector);
if (!isDebug) {
return null;
}
return <WrappedComponent {...(props as any)} />;
return <WrappedComponent {...props} />;
};
3 changes: 3 additions & 0 deletions ts/features/itwallet/common/utils/itwAttestationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const getIntegrityHardwareKeyTag = async (): Promise<string> =>
/**
* Register a new wallet instance with hardwareKeyTag.
* @param hardwareKeyTag - the hardware key tag of the integrity Context
* @param sessionToken - the session token to use for the API calls
*/
export const registerWalletInstance = async (
hardwareKeyTag: string,
Expand All @@ -42,6 +43,7 @@ export const registerWalletInstance = async (
/**
* Getter for the wallet attestation binded to the wallet instance created with the given hardwareKeyTag.
* @param hardwareKeyTag - the hardware key tag of the wallet instance
* @param sessionToken - the session token to use for the API calls
* @return the wallet attestation and the related key tag
*/
export const getAttestation = async (
Expand Down Expand Up @@ -81,6 +83,7 @@ export const isWalletInstanceAttestationValid = (
* Get the wallet instance status from the Wallet Provider.
* This operation is more lightweight than getting a new attestation to check the status.
* @param hardwareKeyTag The hardware key tag used to create the wallet instance
* @param sessionToken The session token to use for the API calls
*/
export const getWalletInstanceStatus = (
hardwareKeyTag: string,
Expand Down
47 changes: 47 additions & 0 deletions ts/features/itwallet/machine/eid/__tests__/machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("itwEidIssuanceMachine", () => {
const navigateToNfcInstructionsScreen = jest.fn();
const navigateToCieIdLoginScreen = jest.fn();
const storeIntegrityKeyTag = jest.fn();
const cleanupIntegrityKeyTag = jest.fn();
const storeWalletInstanceAttestation = jest.fn();
const storeEidCredential = jest.fn();
const closeIssuance = jest.fn();
Expand Down Expand Up @@ -82,6 +83,7 @@ describe("itwEidIssuanceMachine", () => {
navigateToNfcInstructionsScreen,
navigateToCieIdLoginScreen,
storeIntegrityKeyTag,
cleanupIntegrityKeyTag,
storeWalletInstanceAttestation,
storeEidCredential,
closeIssuance,
Expand Down Expand Up @@ -982,4 +984,49 @@ describe("itwEidIssuanceMachine", () => {

expect(actor.getSnapshot().value).toStrictEqual("Idle");
});

it("should cleanup integrity key tag and fail when obtaining Wallet Instance Attestation fails", async () => {
const actor = createActor(mockedMachine);
actor.start();

await waitFor(() => expect(onInit).toHaveBeenCalledTimes(1));

expect(actor.getSnapshot().value).toStrictEqual("Idle");
expect(actor.getSnapshot().context).toStrictEqual(InitialContext);
expect(actor.getSnapshot().tags).toStrictEqual(new Set());

/**
* Start eID issuance
*/
actor.send({ type: "start" });

expect(actor.getSnapshot().value).toStrictEqual("TosAcceptance");
expect(actor.getSnapshot().tags).toStrictEqual(new Set());
expect(navigateToTosScreen).toHaveBeenCalledTimes(1);

/**
* Accept TOS and request WIA
*/

createWalletInstance.mockImplementation(() =>
Promise.resolve(T_INTEGRITY_KEY)
);
getWalletAttestation.mockImplementation(() => Promise.reject({})); // Simulate failure
isSessionExpired.mockImplementation(() => false); // Session not expired

actor.send({ type: "accept-tos" });

expect(actor.getSnapshot().value).toStrictEqual("WalletInstanceCreation");
expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Loading]));

await waitFor(() => expect(createWalletInstance).toHaveBeenCalledTimes(1));
await waitFor(() => expect(getWalletAttestation).toHaveBeenCalledTimes(1));

// Wallet Instance Attestation failure triggers cleanupIntegrityKeyTag
expect(cleanupIntegrityKeyTag).toHaveBeenCalledTimes(1);

// Check that the machine transitions to Failure state
expect(actor.getSnapshot().value).toStrictEqual("Failure");
expect(actor.getSnapshot().tags).toStrictEqual(new Set());
});
});
10 changes: 9 additions & 1 deletion ts/features/itwallet/machine/eid/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { checkCurrentSession } from "../../../../store/actions/authentication";
import { useIOStore } from "../../../../store/hooks";
import { assert } from "../../../../utils/assert";
import { itwCredentialsStore } from "../../credentials/store/actions";
import { itwStoreIntegrityKeyTag } from "../../issuance/store/actions";
import {
itwRemoveIntegrityKeyTag,
itwStoreIntegrityKeyTag
} from "../../issuance/store/actions";
import {
itwLifecycleStateUpdated,
itwLifecycleWalletReset
Expand Down Expand Up @@ -168,6 +171,11 @@ export const createEidIssuanceActionsImplementation = (
store.dispatch(itwStoreIntegrityKeyTag(context.integrityKeyTag));
},

cleanupIntegrityKeyTag: () => {
// Remove the integrity key tag from the store
store.dispatch(itwRemoveIntegrityKeyTag());
},

storeWalletInstanceAttestation: ({
context
}: ActionArgs<Context, EidIssuanceEvents, EidIssuanceEvents>) => {
Expand Down
3 changes: 2 additions & 1 deletion ts/features/itwallet/machine/eid/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const itwEidIssuanceMachine = setup({
navigateToNfcInstructionsScreen: notImplemented,
navigateToWalletRevocationScreen: notImplemented,
storeIntegrityKeyTag: notImplemented,
cleanupIntegrityKeyTag: notImplemented,
storeWalletInstanceAttestation: notImplemented,
storeEidCredential: notImplemented,
closeIssuance: notImplemented,
Expand Down Expand Up @@ -225,7 +226,7 @@ export const itwEidIssuanceMachine = setup({
target: "#itwEidIssuanceMachine.TosAcceptance"
},
{
actions: "setFailure",
actions: ["setFailure", "cleanupIntegrityKeyTag"],
target: "#itwEidIssuanceMachine.Failure"
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const ItwCredentialOnboardingSection = () => {
);

return (
<>
<View>
<ListItemHeader
label={I18n.t("features.wallet.onboarding.sections.itw")}
/>
Expand All @@ -134,7 +134,7 @@ const ItwCredentialOnboardingSection = () => {
/>
))}
</VStack>
</>
</View>
);
};

Expand Down Expand Up @@ -175,7 +175,7 @@ const OtherCardsOnboardingSection = (props: { showTitle?: boolean }) => {
);

return (
<>
<View>
{props.showTitle && (
<ListItemHeader
label={I18n.t("features.wallet.onboarding.sections.other")}
Expand All @@ -190,7 +190,7 @@ const OtherCardsOnboardingSection = (props: { showTitle?: boolean }) => {
onPress={navigateToPaymentMethodOnboarding}
/>
</VStack>
</>
</View>
);
};

Expand Down
56 changes: 36 additions & 20 deletions ts/features/payments/receipts/screens/ReceiptListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,39 @@ import Animated, {
useSharedValue
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { PaymentsReceiptParamsList } from "../navigation/params";
import { getPaymentsReceiptAction } from "../store/actions";
import { walletReceiptListPotSelector } from "../store/selectors";
import { useIONavigation } from "../../../../navigation/params/AppParamsList";
import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors";
import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction";
import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem";
import {
OperationResultScreenContent,
OperationResultScreenContentProps
} from "../../../../components/screens/OperationResultScreenContent";
import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
import { groupTransactionsByMonth } from "../utils";
import I18n from "../../../../i18n";
import { PaymentsReceiptRoutes } from "../navigation/routes";
import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem";
import { useIONavigation } from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender";
import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors";
import * as analytics from "../analytics";
import { ReceiptsCategoryFilter } from "../types";
import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent";
import { ReceiptFadeInOutAnimationView } from "../components/ReceiptFadeInOutAnimationView";
import { ReceiptListItemTransaction } from "../components/ReceiptListItemTransaction";
import { ReceiptLoadingList } from "../components/ReceiptLoadingList";
import { ReceiptSectionListHeader } from "../components/ReceiptSectionListHeader";
import { PaymentsReceiptParamsList } from "../navigation/params";
import { PaymentsReceiptRoutes } from "../navigation/routes";
import { getPaymentsReceiptAction } from "../store/actions";
import { walletReceiptListPotSelector } from "../store/selectors";
import { ReceiptsCategoryFilter } from "../types";
import { groupTransactionsByMonth } from "../utils";

export type ReceiptListScreenProps = RouteProp<
PaymentsReceiptParamsList,
"PAYMENT_RECEIPT_DETAILS"
>;

type OperationResultEmptyProps = Pick<
OperationResultScreenContentProps,
"title" | "subtitle" | "pictogram"
>;

const AnimatedSectionList = Animated.createAnimatedComponent(
SectionList as new () => SectionList<NoticeListItem>
);
Expand All @@ -59,7 +67,6 @@ const ReceiptListScreen = () => {

const transactionsPot = useIOSelector(walletReceiptListPotSelector);
const isEmpty = useIOSelector(isPaymentsTransactionsEmptySelector);

const isLoading = pot.isLoading(transactionsPot);

const handleNavigateToTransactionDetails = (transaction: NoticeListItem) => {
Expand Down Expand Up @@ -163,14 +170,23 @@ const ReceiptListScreen = () => {
</>
);

const emptyProps: OperationResultEmptyProps =
noticeCategory === "payer"
? {
title: I18n.t("features.payments.transactions.list.emptyPayer.title"),
pictogram: "empty"
}
: {
title: I18n.t("features.payments.transactions.list.empty.title"),
subtitle: I18n.t(
"features.payments.transactions.list.empty.subtitle"
),
pictogram: "emptyArchive"
};

const EmptyStateList = isEmpty ? (
<ReceiptFadeInOutAnimationView>
<OperationResultScreenContent
isHeaderVisible
title={I18n.t("features.payments.transactions.list.empty.title")}
subtitle={I18n.t("features.payments.transactions.list.empty.subtitle")}
pictogram="emptyArchive"
/>
<OperationResultScreenContent isHeaderVisible {...emptyProps} />
</ReceiptFadeInOutAnimationView>
) : undefined;

Expand Down
Loading

0 comments on commit 9d11d1f

Please sign in to comment.