Skip to content

Commit

Permalink
Merge branch 'master' into IOPID-2566-auth-error-screen
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisMattew authored Dec 20, 2024
2 parents 14db7aa + 9321272 commit 836c550
Show file tree
Hide file tree
Showing 21 changed files with 530 additions and 201 deletions.
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
66 changes: 42 additions & 24 deletions ts/features/wallet/components/WalletCardsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ import { useFocusEffect } from "@react-navigation/native";
import * as React from "react";
import { View } from "react-native";
import Animated, { LinearTransition } from "react-native-reanimated";
import { useDebugInfo } from "../../../hooks/useDebugInfo";
import I18n from "../../../i18n";
import { useIONavigation } from "../../../navigation/params/AppParamsList";
import { useIOSelector } from "../../../store/hooks";
import { isItwEnabledSelector } from "../../../store/reducers/backendStatus/remoteConfig";
import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet";
import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone";
import {
ItwEidInfoBottomSheetContent,
ItwEidInfoBottomSheetTitle
} from "../../itwallet/common/components/ItwEidInfoBottomSheetContent";
import { ItwEidLifecycleAlert } from "../../itwallet/common/components/ItwEidLifecycleAlert";
import { ItwFeedbackBanner } from "../../itwallet/common/components/ItwFeedbackBanner";
import { ItwWalletReadyBanner } from "../../itwallet/common/components/ItwWalletReadyBanner";
import { ItwDiscoveryBannerStandalone } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBannerStandalone";
import { itwCredentialsEidStatusSelector } from "../../itwallet/credentials/store/selectors";
import { itwLifecycleIsValidSelector } from "../../itwallet/lifecycle/store/selectors";
import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert";
import {
isWalletEmptySelector,
selectIsWalletCardsLoading,
selectIsWalletLoading,
selectWalletCardsByCategory,
selectWalletCategories,
selectWalletCategoryFilter,
selectWalletItwCards,
selectWalletOtherCards,
shouldRenderWalletEmptyStateSelector
} from "../store/selectors";
import { WalletCardCategoryFilter } from "../types";
import { useItwWalletInstanceRevocationAlert } from "../../itwallet/walletInstance/hook/useItwWalletInstanceRevocationAlert";
import { withWalletCategoryFilter } from "../utils";
import { WalletCardSkeleton } from "./WalletCardSkeleton";
import { WalletCardsCategoryContainer } from "./WalletCardsCategoryContainer";
import { WalletCardsCategoryRetryErrorBanner } from "./WalletCardsCategoryRetryErrorBanner";
import { WalletCardSkeleton } from "./WalletCardSkeleton";
import { WalletEmptyScreenContent } from "./WalletEmptyScreenContent";

const EID_INFO_BOTTOM_PADDING = 128;
Expand All @@ -42,9 +42,8 @@ const EID_INFO_BOTTOM_PADDING = 128;
* and the empty state
*/
const WalletCardsContainer = () => {
const isLoading = useIOSelector(selectIsWalletCardsLoading);
const isLoading = useIOSelector(selectIsWalletLoading);
const isWalletEmpty = useIOSelector(isWalletEmptySelector);
const selectedCategory = useIOSelector(selectWalletCategoryFilter);
const shouldRenderEmptyState = useIOSelector(
shouldRenderWalletEmptyStateSelector
);
Expand All @@ -55,13 +54,6 @@ const WalletCardsContainer = () => {
// placeholders in the wallet
const shouldRenderLoadingState = isLoading && isWalletEmpty;

// Returns true if no category filter is selected or if the filter matches the given category
const shouldRenderCategory = React.useCallback(
(filter: WalletCardCategoryFilter): boolean =>
selectedCategory === undefined || selectedCategory === filter,
[selectedCategory]
);

// Content to render in the wallet screen, based on the current state
const walletContent = React.useMemo(() => {
if (shouldRenderLoadingState) {
Expand All @@ -72,11 +64,11 @@ const WalletCardsContainer = () => {
}
return (
<View testID="walletCardsContainerTestID" style={IOStyles.flex}>
{shouldRenderCategory("itw") && <ItwWalletCardsContainer />}
{shouldRenderCategory("other") && <OtherWalletCardsContainer />}
<ItwWalletCardsContainer />
<OtherWalletCardsContainer />
</View>
);
}, [shouldRenderEmptyState, shouldRenderCategory, shouldRenderLoadingState]);
}, [shouldRenderEmptyState, shouldRenderLoadingState]);

return (
<Animated.View
Expand All @@ -89,6 +81,9 @@ const WalletCardsContainer = () => {
);
};

/**
* Skeleton for the wallet cards container
*/
const WalletCardsContainerSkeleton = () => (
<>
<WalletCardSkeleton testID="walletCardSkeletonTestID_1" cardProps={{}} />
Expand All @@ -97,15 +92,29 @@ const WalletCardsContainerSkeleton = () => (
</>
);

const ItwWalletCardsContainer = () => {
/**
* Card container for the ITW credentials
*/
const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => {
const navigation = useIONavigation();
const cards = useIOSelector(selectWalletItwCards);
const cards = useIOSelector(state =>
selectWalletCardsByCategory(state, "itw")
);
const isItwValid = useIOSelector(itwLifecycleIsValidSelector);
const isItwEnabled = useIOSelector(isItwEnabledSelector);
const eidStatus = useIOSelector(itwCredentialsEidStatusSelector);

const isEidExpired = eidStatus === "jwtExpired";

useDebugInfo({
itw: {
isItwValid,
isItwEnabled,
eidStatus,
cards
}
});

const eidInfoBottomSheet = useIOBottomSheetAutoresizableModal(
{
title: <ItwEidInfoBottomSheetTitle isExpired={isEidExpired} />,
Expand Down Expand Up @@ -172,12 +181,21 @@ const ItwWalletCardsContainer = () => {
{isItwValid && eidInfoBottomSheet.bottomSheet}
</>
);
};
});

const OtherWalletCardsContainer = () => {
/**
* Card container for the other cards (payments, bonus, etc.)
*/
const OtherWalletCardsContainer = withWalletCategoryFilter("other", () => {
const cards = useIOSelector(selectWalletOtherCards);
const categories = useIOSelector(selectWalletCategories);

useDebugInfo({
other: {
cards
}
});

const sectionHeader = React.useMemo((): ListItemHeader | undefined => {
// The section header must be displayed only if there are more categories
if (categories.size <= 1) {
Expand All @@ -203,7 +221,7 @@ const OtherWalletCardsContainer = () => {
bottomElement={<WalletCardsCategoryRetryErrorBanner />}
/>
);
};
});

export {
ItwWalletCardsContainer,
Expand Down
Loading

0 comments on commit 836c550

Please sign in to comment.