Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1852] Hide discovery banner after close (#6499)
Browse files Browse the repository at this point in the history
## Short description
This PR hides the discovery banner in the messages screen for six months
after the user closes it.

Note that only the closable banner in the messages screen stays hidden,
the one in the wallet screen is always visible.

## List of changes proposed in this pull request
- Added `hideDiscoveryBannerUntilDate` key to the `preferences` reducer
- Changed the selector in `landingScreenBannerMap`

## How to test
- Close the discovery banner in the messages screen and reload the app:
it should be visible anymore
- Ensure the discovery banner in the wallet screen is still visible

---------

Co-authored-by: Federico Mastrini <[email protected]>
  • Loading branch information
gispada and mastro993 authored Dec 6, 2024
1 parent 720e65b commit 6d87026
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
trackITWalletBannerVisualized
} from "../../../analytics";
import { ITW_ROUTES } from "../../../navigation/routes";
import { useIODispatch } from "../../../../../store/hooks";
import { itwCloseDiscoveryBanner } from "../../store/actions/preferences";

/**
* to use in flows where we want to handle the banner's visibility logic externally
Expand All @@ -31,6 +33,7 @@ export const ItwDiscoveryBanner = ({
handleOnClose
}: ItwDiscoveryBannerProps) => {
const bannerRef = React.createRef<View>();
const dispatch = useIODispatch();

const navigation = useIONavigation();
const route = useRoute();
Expand All @@ -56,6 +59,7 @@ export const ItwDiscoveryBanner = ({
const handleClose = () => {
trackItWalletBannerClosure(trackBannerProperties);
handleOnClose?.();
dispatch(itwCloseDiscoveryBanner());
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,32 @@
import React, { ReactElement } from "react";
import { View } from "react-native";
import React from "react";
import { StyleSheet, View } from "react-native";
import { useIOSelector } from "../../../../../store/hooks";
import { isItwDiscoveryBannerRenderableSelector } from "../../store/selectors";
import {
ItwDiscoveryBanner,
ItwDiscoveryBannerProps
} from "./ItwDiscoveryBanner";

type Props = Omit<ItwDiscoveryBannerProps, "handleOnClose"> & {
fallbackComponent?: ReactElement;
};
import { ItwDiscoveryBanner } from "./ItwDiscoveryBanner";

/**
* to use in flows where either
* - we need a fallback component
* - we do not want to handle the banner's visibility logic externally
* (see MultiBanner feature for the landing screen)
* to use in flows where we do not want to handle the banner's visibility logic externally
* (see MultiBanner feature for the landing screen)
*/
export const ItwDiscoveryBannerStandalone = (props: Props) => {
const [isVisible, setVisible] = React.useState(true);

export const ItwDiscoveryBannerStandalone = () => {
const isBannerRenderable = useIOSelector(
isItwDiscoveryBannerRenderableSelector
);

const shouldBeHidden = React.useMemo(
() =>
// Banner should be hidden if:
!isVisible || // The user closed it by pressing the `x` button
!isBannerRenderable, // the various validity checks fail
[isBannerRenderable, isVisible]
);

if (shouldBeHidden) {
const { fallbackComponent } = props;
if (fallbackComponent) {
return fallbackComponent;
}
if (!isBannerRenderable) {
return null;
}

return (
<View style={{ marginTop: 16, marginBottom: 8 }}>
<ItwDiscoveryBanner
ignoreMargins={true}
handleOnClose={() => setVisible(false)}
{...props}
/>
<View style={styles.wrapper}>
<ItwDiscoveryBanner ignoreMargins={true} closable={false} />
</View>
);
};

const styles = StyleSheet.create({
wrapper: {
marginTop: 16,
marginBottom: 8
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ type RenderOptions = {
isItwEnabled?: boolean;
};

jest.mock("../../../../../../config", () => ({
itwEnabled: true
}));

describe("ItwDiscoveryBanner", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
const component = renderScreenWithNavigationStoreContext<GlobalState>(
Expand Down
8 changes: 7 additions & 1 deletion ts/features/itwallet/common/store/actions/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ export const itwCloseFeedbackBanner = createStandardAction(
"ITW_CLOSE_FEEDBACK_BANNER"
)();

export type ItwPreferencesActions = ActionType<typeof itwCloseFeedbackBanner>;
export const itwCloseDiscoveryBanner = createStandardAction(
"ITW_CLOSE_DISCOVERY_BANNER"
)();

export type ItwPreferencesActions =
| ActionType<typeof itwCloseFeedbackBanner>
| ActionType<typeof itwCloseDiscoveryBanner>;
14 changes: 13 additions & 1 deletion ts/features/itwallet/common/store/reducers/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { addMonths } from "date-fns";
import { getType } from "typesafe-actions";
import { Action } from "../../../../../store/actions/types";
import { itwCloseFeedbackBanner } from "../actions/preferences";
import {
itwCloseDiscoveryBanner,
itwCloseFeedbackBanner
} from "../actions/preferences";

export type ItwPreferencesState = {
hideFeedbackBannerUntilDate?: string;
hideDiscoveryBannerUntilDate?: string;
};

const INITIAL_STATE: ItwPreferencesState = {};
Expand All @@ -16,10 +20,18 @@ const reducer = (
switch (action.type) {
case getType(itwCloseFeedbackBanner): {
return {
...state,
hideFeedbackBannerUntilDate: addMonths(new Date(), 1).toISOString()
};
}

case getType(itwCloseDiscoveryBanner): {
return {
...state,
hideDiscoveryBannerUntilDate: addMonths(new Date(), 6).toISOString()
};
}

default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { addDays, addMonths } from "date-fns";
import _ from "lodash";
import { applicationChangeState } from "../../../../../../store/actions/application";
import { appReducer } from "../../../../../../store/reducers";
import { itwIsFeedbackBannerHiddenSelector } from "../preferences";
import {
itwIsDiscoveryBannerHiddenSelector,
itwIsFeedbackBannerHiddenSelector
} from "../preferences";

describe("itwIsFeedbackBannerHiddenSelector", () => {
it.each([
Expand All @@ -24,3 +27,23 @@ describe("itwIsFeedbackBannerHiddenSelector", () => {
).toBe(expected);
});
});

describe("itwIsDiscoveryBannerHiddenSelector", () => {
it.each([
[false, undefined],
[false, "definitely not a date"],
[false, new Date().toISOString()],
[false, addDays(new Date(), -2).toISOString()],
[true, addMonths(new Date(), 2).toISOString()]
])("should return %p if banner is hidden until %p", (expected, value) => {
const globalState = appReducer(undefined, applicationChangeState("active"));

expect(
itwIsDiscoveryBannerHiddenSelector(
_.set(globalState, "features.itWallet.preferences", {
hideDiscoveryBannerUntilDate: value
})
)
).toBe(expected);
});
});
15 changes: 14 additions & 1 deletion ts/features/itwallet/common/store/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
import { GlobalState } from "../../../../../store/reducers/types";
import { itwIsWalletEmptySelector } from "../../../credentials/store/selectors";
import { itwLifecycleIsValidSelector } from "../../../lifecycle/store/selectors";
import { itwIsFeedbackBannerHiddenSelector } from "./preferences";
import {
itwIsFeedbackBannerHiddenSelector,
itwIsDiscoveryBannerHiddenSelector
} from "./preferences";

/**
* Returns if the discovery banner should be rendered. The banner is rendered if:
Expand All @@ -17,6 +20,16 @@ import { itwIsFeedbackBannerHiddenSelector } from "./preferences";
export const isItwDiscoveryBannerRenderableSelector = (state: GlobalState) =>
!itwLifecycleIsValidSelector(state) && isItwEnabledSelector(state);

/**
* Returns the renderable state of the discovery banner with the persisted user's preference:
* after being closed by the user it should stay hidden for 6 months.
*/
export const isItwPersistedDiscoveryBannerRenderableSelector = (
state: GlobalState
) =>
!itwIsDiscoveryBannerHiddenSelector(state) &&
isItwDiscoveryBannerRenderableSelector(state);

/**
* Returns if the feedback banner should be visible. The banner is visible if:
* - The Wallet has valid Wallet Instance and a valid eID
Expand Down
23 changes: 15 additions & 8 deletions ts/features/itwallet/common/store/selectors/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { createSelector } from "reselect";
import { GlobalState } from "../../../../../store/reducers/types";
import { ItwPreferencesState } from "../reducers/preferences";

const isPastDate = (date?: string) => {
if (!date) {
return false;
}
const hideUntilDate = new Date(date);
return !isNaN(hideUntilDate.getTime()) && !isPast(hideUntilDate);
};

export const itwPreferencesSelector = (state: GlobalState) =>
state.features.itWallet.preferences;

Expand All @@ -13,13 +21,12 @@ export const itwPreferencesSelector = (state: GlobalState) =>
*/
export const itwIsFeedbackBannerHiddenSelector = createSelector(
itwPreferencesSelector,
({ hideFeedbackBannerUntilDate }: ItwPreferencesState) => {
if (!hideFeedbackBannerUntilDate) {
return false;
}

const hideUntilDate = new Date(hideFeedbackBannerUntilDate);
({ hideFeedbackBannerUntilDate }: ItwPreferencesState) =>
isPastDate(hideFeedbackBannerUntilDate)
);

return !isNaN(hideUntilDate.getTime()) && !isPast(hideUntilDate);
}
export const itwIsDiscoveryBannerHiddenSelector = createSelector(
itwPreferencesSelector,
({ hideDiscoveryBannerUntilDate }: ItwPreferencesState) =>
isPastDate(hideDiscoveryBannerUntilDate)
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from "react";
import { SettingsDiscoveryBanner } from "../../../screens/profile/components/SettingsDiscoveryBanner";
import { GlobalState } from "../../../store/reducers/types";
import { ItwDiscoveryBanner } from "../../itwallet/common/components/discoveryBanner/ItwDiscoveryBanner";
import { isItwDiscoveryBannerRenderableSelector } from "../../itwallet/common/store/selectors";
import { isItwPersistedDiscoveryBannerRenderableSelector } from "../../itwallet/common/store/selectors";
import { hasUserAcknowledgedSettingsBannerSelector } from "../../profileSettings/store/selectors";
import { PushNotificationsBanner } from "../../pushNotifications/components/PushNotificationsBanner";
import { isPushNotificationsBannerRenderableSelector } from "../../pushNotifications/store/selectors";
Expand Down Expand Up @@ -38,7 +38,7 @@ export const landingScreenBannerMap: BannerMapById = {
component: closeHandler => (
<ItwDiscoveryBanner closable handleOnClose={closeHandler} />
),
isRenderableSelector: isItwDiscoveryBannerRenderableSelector
isRenderableSelector: isItwPersistedDiscoveryBannerRenderableSelector
},
SETTINGS_DISCOVERY: {
component: closeHandler => (
Expand Down
4 changes: 2 additions & 2 deletions ts/features/wallet/components/WalletCardsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const WalletCardsContainer = () => {
// the wallet is not in a loading state anymore
return (
<View style={IOStyles.flex}>
<ItwDiscoveryBannerStandalone closable={false} />
<ItwDiscoveryBannerStandalone />
<WalletEmptyScreenContent />
</View>
);
Expand All @@ -80,7 +80,7 @@ const WalletCardsContainer = () => {
layout={LinearTransition.duration(200)}
>
<View testID="walletCardsContainerTestID">
<ItwDiscoveryBannerStandalone closable={false} />
<ItwDiscoveryBannerStandalone />
{shouldRender("itw") && <ItwCardsContainer />}
{shouldRender("other") && <OtherCardsContainer />}
</View>
Expand Down

0 comments on commit 6d87026

Please sign in to comment.