Skip to content

Commit

Permalink
chore(IT Wallet): [SIW-1937] Improve wallet category filters (#6570)
Browse files Browse the repository at this point in the history
## Short description
This PR fixes a bug in the wallet screen where users were unable to see
any screen content if the category filters were in the wrong state. This
PR also improves the debug data overlay and the wallet redux selectors.

### Steps to reproduce the addressed bug
- Navigate to the Wallet screen, ensure you have *Documenti su IO*
enabled and at least one payment method saved
- Select the "Other" category filter
- Remove everything but ITW from the wallet, keeping the "Other" filter
selected
- Once you removed everything you should see a blank screen
- Restarting the app should not fix the issue

## List of changes proposed in this pull request
- Added `isWalletCategoryFilteringEnabledSelector` selector to check if
the category filtering is available: filtering is available only if
there is more than one category in the wallet.
- Added `shouldRenderWalletCategorySelector` selector to check if a
wallet category section should be rendered, based on the currently
selected filter and the number of categories available in the wallet
- Added `withWalletCategoryFilter` HOC, which display a component based
on the given category filter
- Added `selectWalletCardsByCategory` and `selectWalletCardsByType` to
select cards based on category or type, removing the need to have
dedicated selectors
- Removed `selectWalletCgnCard` and `selectBonusCards` selectors
- Improved debug data in `DebugInfoOverlay`
  - Added the ability to display `Set` objects
- Added the ability to add data from different components at the same
time by merging data using the same key
- Improved wallet category filtering
-  Refactored `WalletCardsContainer` with new selectors
- Added useful/missing debug data 
- Added tests
- Removed `categoryFilter` preference persistence from the
`wallet.preferences` feature reducer

## How to test
- Static checks should pass
- Navigate to the wallet screen and check that everything works fine,
especially the category filters
- Try to reproduce the bug, you should now see the wallet content.
  • Loading branch information
mastro993 authored Dec 20, 2024
1 parent dcde5de commit 9321272
Show file tree
Hide file tree
Showing 17 changed files with 469 additions and 199 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} />;
};
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
24 changes: 17 additions & 7 deletions ts/features/wallet/components/WalletCategoryFilterTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks";
import { trackWalletCategoryFilter } from "../../itwallet/analytics";
import { walletSetCategoryFilter } from "../store/actions/preferences";
import {
selectWalletCategories,
isWalletCategoryFilteringEnabledSelector,
selectWalletCategoryFilter
} from "../store/selectors";
import { walletCardCategoryFilters } from "../types";
import { useDebugInfo } from "../../../hooks/useDebugInfo";

/**
* Renders filter tabs to categorize cards on the wallet home screen.
Expand All @@ -23,18 +24,27 @@ import { walletCardCategoryFilters } from "../types";
const WalletCategoryFilterTabs = () => {
const dispatch = useIODispatch();

const selectedCategory = useIOSelector(selectWalletCategoryFilter);
const categories = useIOSelector(selectWalletCategories);
const categoryFilter = useIOSelector(selectWalletCategoryFilter);
const isFilteringEnabled = useIOSelector(
isWalletCategoryFilteringEnabledSelector
);

useDebugInfo({
wallet: {
isFilteringEnabled,
categoryFilter
}
});

const selectedIndex = React.useMemo(
() =>
selectedCategory
? walletCardCategoryFilters.indexOf(selectedCategory) + 1
categoryFilter
? walletCardCategoryFilters.indexOf(categoryFilter) + 1
: 0,
[selectedCategory]
[categoryFilter]
);

if (categories.size <= 1) {
if (!isFilteringEnabled) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe("WalletCardsContainer", () => {

it("should render the loading screen", () => {
jest
.spyOn(walletSelectors, "selectIsWalletCardsLoading")
.spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => true);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
Expand All @@ -117,7 +117,7 @@ describe("WalletCardsContainer", () => {

it("should render the empty screen", () => {
jest
.spyOn(walletSelectors, "selectIsWalletCardsLoading")
.spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => false);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
Expand Down Expand Up @@ -150,14 +150,14 @@ describe("WalletCardsContainer", () => {
.mockImplementation(() => [T_CARDS["1"], T_CARDS["2"], T_CARDS["3"]]);

jest
.spyOn(walletSelectors, "selectWalletItwCards")
.spyOn(walletSelectors, "selectWalletCardsByCategory")
.mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]);

jest
.spyOn(configSelectors, "isItwEnabledSelector")
.mockImplementation(() => true);
jest
.spyOn(walletSelectors, "selectIsWalletCardsLoading")
.spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => false);
jest
.spyOn(walletSelectors, "selectWalletCategoryFilter")
Expand Down Expand Up @@ -193,7 +193,7 @@ describe("WalletCardsContainer", () => {
.mockImplementation(() => true);

jest
.spyOn(walletSelectors, "selectIsWalletCardsLoading")
.spyOn(walletSelectors, "selectIsWalletLoading")
.mockImplementation(() => isLoading);
jest
.spyOn(walletSelectors, "shouldRenderWalletEmptyStateSelector")
Expand Down Expand Up @@ -244,7 +244,7 @@ describe("ItwWalletCardsContainer", () => {
.spyOn(configSelectors, "isItwEnabledSelector")
.mockImplementation(() => true);
jest
.spyOn(walletSelectors, "selectWalletItwCards")
.spyOn(walletSelectors, "selectWalletCardsByCategory")
.mockImplementation(() => [T_CARDS["4"], T_CARDS["5"]]);

const { queryByTestId } = renderComponent(ItwWalletCardsContainer);
Expand Down
Loading

0 comments on commit 9321272

Please sign in to comment.