diff --git a/ts/features/landingScreenMultiBanner/components/LandingScreenBannerPicker.tsx b/ts/features/landingScreenMultiBanner/components/LandingScreenBannerPicker.tsx
index e0c5be7a579..7d91aed71ad 100644
--- a/ts/features/landingScreenMultiBanner/components/LandingScreenBannerPicker.tsx
+++ b/ts/features/landingScreenMultiBanner/components/LandingScreenBannerPicker.tsx
@@ -3,6 +3,7 @@ import { useIODispatch, useIOSelector } from "../../../store/hooks";
import { landingScreenBannerToRenderSelector } from "../store/selectors";
import { updateLandingScreenBannerVisibility } from "../store/actions";
import { landingScreenBannerMap } from "../utils/landingScreenBannerMap";
+import { usePushNotificationsBannerTracking } from "../../pushNotifications/hooks/usePushNotificationsBannerTracking";
export const LandingScreenBannerPicker = () => {
const dispatch = useIODispatch();
@@ -19,6 +20,8 @@ export const LandingScreenBannerPicker = () => {
}
}, [bannerToRender, dispatch]);
+ usePushNotificationsBannerTracking();
+
if (bannerToRender === undefined) {
return null;
}
diff --git a/ts/features/landingScreenMultiBanner/components/__tests__/LandingScreenBannerPicker.test.tsx b/ts/features/landingScreenMultiBanner/components/__tests__/LandingScreenBannerPicker.test.tsx
index 852000fdb2c..952e73aa596 100644
--- a/ts/features/landingScreenMultiBanner/components/__tests__/LandingScreenBannerPicker.test.tsx
+++ b/ts/features/landingScreenMultiBanner/components/__tests__/LandingScreenBannerPicker.test.tsx
@@ -12,6 +12,7 @@ import { updateLandingScreenBannerVisibility } from "../../store/actions";
import * as SELECTORS from "../../store/selectors";
import { LandingScreenBannerId } from "../../utils/landingScreenBannerMap";
import { LandingScreenBannerPicker } from "../LandingScreenBannerPicker";
+import * as hooks from "../../../pushNotifications/hooks/usePushNotificationsBannerTracking";
jest.mock("../../utils/landingScreenBannerMap", () => ({
get landingScreenBannerMap() {
@@ -89,6 +90,16 @@ describe("LandingBannerPicker", () => {
updateLandingScreenBannerVisibility({ id: bannerId, enabled: false })
);
});
+ it("should include 'usePushNotificationsBannerTracking'", () => {
+ const spyUsePushNotificationsBannerTracking = jest.spyOn(
+ hooks,
+ "usePushNotificationsBannerTracking"
+ );
+
+ renderComponent();
+
+ expect(spyUsePushNotificationsBannerTracking.mock.calls.length).toBe(1);
+ });
});
const renderComponent = () => {
diff --git a/ts/features/pushNotifications/analytics/__tests__/index.test.ts b/ts/features/pushNotifications/analytics/__tests__/index.test.ts
index 6ecbef8c54a..cf2640ebf75 100644
--- a/ts/features/pushNotifications/analytics/__tests__/index.test.ts
+++ b/ts/features/pushNotifications/analytics/__tests__/index.test.ts
@@ -7,6 +7,14 @@ import {
trackNotificationsOptInReminderOnPermissionsOff,
trackNotificationsOptInReminderStatus,
trackNotificationsOptInSkipSystemPermissions,
+ trackPushNotificationBannerDismissAlert,
+ trackPushNotificationBannerDismissOutcome,
+ trackPushNotificationBannerForceShow,
+ trackPushNotificationBannerStillHidden,
+ trackPushNotificationsBannerClosure,
+ trackPushNotificationsBannerTap,
+ trackPushNotificationsBannerVisualized,
+ trackPushNotificationSystemPopupShown,
trackPushNotificationTokenUploadFailure,
trackPushNotificationTokenUploadSucceeded,
trackSystemNotificationPermissionScreenOutcome,
@@ -15,6 +23,8 @@ import {
import { PushNotificationsContentTypeEnum } from "../../../../../definitions/backend/PushNotificationsContentType";
import { ReminderStatusEnum } from "../../../../../definitions/backend/ReminderStatus";
import * as Mixpanel from "../../../../mixpanel";
+import ROUTES from "../../../../navigation/routes";
+import { MESSAGES_ROUTES } from "../../../messages/navigation/routes";
describe("pushNotifications analytics", () => {
beforeEach(() => {
@@ -236,6 +246,136 @@ describe("pushNotifications analytics", () => {
new_notification_status: notificationPermissionsEnabled
});
});
+ [MESSAGES_ROUTES.MESSAGES_HOME, ROUTES.SETTINGS_MAIN].forEach(route => {
+ it(`'trackPushNotificationsBannerVisualized' should have expected event name and properties for '${route}'`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationsBannerVisualized(route);
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "screen_view",
+ banner_id: "push_notif_activation",
+ banner_page:
+ route === "MESSAGES_HOME" ? "MESSAGES_HOME" : "SETTINGS_MAIN",
+ banner_landing: "os_notification_settings"
+ });
+ });
+ it(`'trackPushNotificationsBannerTap' should have expected event name and properties for '${route}'`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationsBannerTap(route);
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("TAP_BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "action",
+ banner_id: "push_notif_activation",
+ banner_page:
+ route === "MESSAGES_HOME" ? "MESSAGES_HOME" : "SETTINGS_MAIN",
+ banner_landing: "os_notification_settings"
+ });
+ });
+ });
+ it(`'trackPushNotificationsBannerClosure' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationsBannerClosure();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("CLOSE_BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "action",
+ banner_id: "push_notif_activation",
+ banner_page: "MESSAGES_HOME",
+ banner_landing: "os_notification_settings"
+ });
+ });
+ it(`'trackPushNotificationSystemPopupShown' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationSystemPopupShown();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("PUSH_NOTIF_SYSTEM_ALERT");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "screen_view"
+ });
+ });
+ it(`'trackPushNotificationBannerDismissAlert' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationBannerDismissAlert();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe(
+ "PUSH_NOTIF_THIRD_DISMISS_ALERT"
+ );
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "screen_view"
+ });
+ });
+ ["deactivate" as const, "remind_later" as const].forEach(outcome =>
+ it(`'trackPushNotificationBannerDismissOutcome' should have expected event name and properties for '${outcome}'`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationBannerDismissOutcome(outcome);
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe(
+ "PUSH_NOTIF_THIRD_DISMISS_ALERT_INTERACTION"
+ );
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "action",
+ outcome
+ });
+ })
+ );
+ it(`'trackPushNotificationBannerForceShow' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackPushNotificationBannerForceShow();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe(
+ "PUSH_NOTIF_BANNER_FORCE_SHOW"
+ );
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "TECH",
+ event_type: undefined
+ });
+ });
+ it(`'trackPushNotificationBannerStillHidden' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+ const unreadMessagesCount = 3;
+
+ void trackPushNotificationBannerStillHidden(unreadMessagesCount);
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe(
+ "PUSH_NOTIF_BANNER_STILL_HIDDEN"
+ );
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "TECH",
+ event_type: undefined,
+ unread_count: unreadMessagesCount
+ });
+ });
});
const getMockMixpanelTrack = () =>
diff --git a/ts/features/pushNotifications/analytics/index.ts b/ts/features/pushNotifications/analytics/index.ts
index aa1f7bbc395..6203c989447 100644
--- a/ts/features/pushNotifications/analytics/index.ts
+++ b/ts/features/pushNotifications/analytics/index.ts
@@ -1,7 +1,9 @@
import { PushNotificationsContentTypeEnum } from "../../../../definitions/backend/PushNotificationsContentType";
import { ReminderStatusEnum } from "../../../../definitions/backend/ReminderStatus";
import { mixpanelTrack } from "../../../mixpanel";
+import ROUTES from "../../../navigation/routes";
import { buildEventProperties } from "../../../utils/analytics";
+import { MESSAGES_ROUTES } from "../../messages/navigation/routes";
export const trackNotificationInstallationTokenNotChanged = () =>
void mixpanelTrack(
@@ -90,3 +92,73 @@ export const trackNotificationPermissionsStatus = (
});
void mixpanelTrack(eventName, props);
};
+
+export const trackPushNotificationsBannerVisualized = (
+ bannerPage: typeof MESSAGES_ROUTES.MESSAGES_HOME | typeof ROUTES.SETTINGS_MAIN
+) => {
+ const eventName = "BANNER";
+ const props = buildEventProperties("UX", "screen_view", {
+ banner_id: "push_notif_activation",
+ banner_page: bannerPage,
+ banner_landing: "os_notification_settings"
+ });
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationsBannerTap = (
+ bannerPage: typeof MESSAGES_ROUTES.MESSAGES_HOME | typeof ROUTES.SETTINGS_MAIN
+) => {
+ const eventName = "TAP_BANNER";
+ const props = buildEventProperties("UX", "action", {
+ banner_id: "push_notif_activation",
+ banner_page: bannerPage,
+ banner_landing: "os_notification_settings"
+ });
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationsBannerClosure = () => {
+ const eventName = "CLOSE_BANNER";
+ const props = buildEventProperties("UX", "action", {
+ banner_id: "push_notif_activation",
+ banner_page: MESSAGES_ROUTES.MESSAGES_HOME,
+ banner_landing: "os_notification_settings"
+ });
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationSystemPopupShown = () => {
+ const eventName = "PUSH_NOTIF_SYSTEM_ALERT";
+ const props = buildEventProperties("UX", "screen_view");
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationBannerDismissAlert = () => {
+ const eventName = "PUSH_NOTIF_THIRD_DISMISS_ALERT";
+ const props = buildEventProperties("UX", "screen_view");
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationBannerDismissOutcome = (
+ outcome: "deactivate" | "remind_later"
+) => {
+ const eventName = "PUSH_NOTIF_THIRD_DISMISS_ALERT_INTERACTION";
+ const props = buildEventProperties("UX", "action", { outcome });
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationBannerForceShow = () => {
+ const eventName = "PUSH_NOTIF_BANNER_FORCE_SHOW";
+ const props = buildEventProperties("TECH", undefined);
+ void mixpanelTrack(eventName, props);
+};
+
+export const trackPushNotificationBannerStillHidden = (
+ unreadMessagesCount: number
+) => {
+ const eventName = "PUSH_NOTIF_BANNER_STILL_HIDDEN";
+ const props = buildEventProperties("TECH", undefined, {
+ unread_count: unreadMessagesCount
+ });
+ void mixpanelTrack(eventName, props);
+};
diff --git a/ts/features/pushNotifications/components/PushNotificationsBanner.tsx b/ts/features/pushNotifications/components/PushNotificationsBanner.tsx
index 410bb0d02d1..94ed4de84df 100644
--- a/ts/features/pushNotifications/components/PushNotificationsBanner.tsx
+++ b/ts/features/pushNotifications/components/PushNotificationsBanner.tsx
@@ -19,6 +19,15 @@ import {
shouldResetNotificationBannerDismissStateSelector,
timesPushNotificationBannerDismissedSelector
} from "../store/selectors/notificationsBannerDismissed";
+import { MESSAGES_ROUTES } from "../../messages/navigation/routes";
+import {
+ trackPushNotificationBannerDismissAlert,
+ trackPushNotificationBannerDismissOutcome,
+ trackPushNotificationBannerForceShow,
+ trackPushNotificationsBannerClosure,
+ trackPushNotificationsBannerTap,
+ trackPushNotificationsBannerVisualized
+} from "../analytics";
type Props = {
closeHandler: () => void;
};
@@ -31,9 +40,13 @@ export const PushNotificationsBanner = ({ closeHandler }: Props) => {
React.useEffect(() => {
if (shouldResetDismissState) {
+ trackPushNotificationBannerForceShow();
dispatch(resetNotificationBannerDismissState());
}
}, [dispatch, shouldResetDismissState]);
+ React.useEffect(() => {
+ trackPushNotificationsBannerVisualized(MESSAGES_ROUTES.MESSAGES_HOME);
+ }, []);
const dismissionCount = useIOSelector(
timesPushNotificationBannerDismissedSelector
@@ -41,7 +54,9 @@ export const PushNotificationsBanner = ({ closeHandler }: Props) => {
const discardModal = usePushNotificationsBannerBottomSheet(closeHandler);
const onClose = () => {
+ trackPushNotificationsBannerClosure();
if (dismissionCount >= 2) {
+ trackPushNotificationBannerDismissAlert();
discardModal.present();
} else {
dispatch(setUserDismissedNotificationsBanner());
@@ -49,6 +64,11 @@ export const PushNotificationsBanner = ({ closeHandler }: Props) => {
}
};
+ const onPress = React.useCallback(() => {
+ trackPushNotificationsBannerTap(MESSAGES_ROUTES.MESSAGES_HOME);
+ openSystemNotificationSettingsScreen();
+ }, []);
+
return (
{
size="big"
onClose={onClose}
labelClose={I18n.t("global.buttons.close")}
- onPress={openSystemNotificationSettingsScreen}
+ onPress={onPress}
/>
{discardModal.bottomSheet}
@@ -73,8 +93,15 @@ const usePushNotificationsBannerBottomSheet = (
) => {
const dispatch = useIODispatch();
- const fullCloseHandler = () =>
+ const internalRemindLaterHandler = () => {
+ trackPushNotificationBannerDismissOutcome("remind_later");
+ remindLaterHandler();
+ };
+
+ const fullCloseHandler = () => {
+ trackPushNotificationBannerDismissOutcome("deactivate");
dispatch(setPushNotificationBannerForceDismissed());
+ };
return useIOBottomSheetModal({
title: I18n.t(
@@ -88,7 +115,7 @@ const usePushNotificationsBannerBottomSheet = (
label: I18n.t(
"features.messages.pushNotifications.banner.bottomSheet.cta"
),
- onPress: remindLaterHandler
+ onPress: internalRemindLaterHandler
},
secondary: {
label: I18n.t(
diff --git a/ts/features/pushNotifications/components/__tests__/PushNotificationsBanner.test.tsx b/ts/features/pushNotifications/components/__tests__/PushNotificationsBanner.test.tsx
index 3c3c66684cc..439cadeaa61 100644
--- a/ts/features/pushNotifications/components/__tests__/PushNotificationsBanner.test.tsx
+++ b/ts/features/pushNotifications/components/__tests__/PushNotificationsBanner.test.tsx
@@ -1,6 +1,7 @@
import { fireEvent } from "@testing-library/react-native";
import * as React from "react";
import { createStore } from "redux";
+import { constUndefined } from "fp-ts/lib/function";
import I18n from "../../../../i18n";
import { applicationChangeState } from "../../../../store/actions/application";
import * as IOHOOKS from "../../../../store/hooks";
@@ -13,41 +14,72 @@ import * as SELECTORS from "../../store/selectors";
import * as NOTIFICATION_DISMISS_SELECTORS from "../../store/selectors/notificationsBannerDismissed";
import * as UTILS from "../../utils";
import { PushNotificationsBanner } from "../PushNotificationsBanner";
+import * as analytics from "../../analytics";
+import { setPushNotificationBannerForceDismissed } from "../../store/actions/userBehaviour";
-const testPressHandler = jest.fn();
-jest
- .spyOn(UTILS, "openSystemNotificationSettingsScreen")
- .mockImplementation(testPressHandler);
describe("PushNotificationsBanner", () => {
- beforeEach(() => {
- jest.clearAllMocks();
+ afterEach(() => {
+ jest.restoreAllMocks();
});
- it("should render correctly", () => {
+ it("should render correctly and call 'trackPushNotificationsBannerVisualized'", () => {
+ const spyOnMockedTrackPushNotificationsBannerVisualized = jest
+ .spyOn(analytics, "trackPushNotificationsBannerVisualized")
+ .mockImplementation(_ => undefined);
const component = renderTestingComponent(
null} />
);
expect(component.toJSON()).toMatchSnapshot();
+ expect(
+ spyOnMockedTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockedTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledWith(MESSAGES_ROUTES.MESSAGES_HOME);
});
it("should call openSystemNotificationSettingsScreen on press", () => {
+ const spyOnMockedOpenSystemNotificationSettingsScreen = jest
+ .spyOn(UTILS, "openSystemNotificationSettingsScreen")
+ .mockImplementation(constUndefined);
+ const spyOnMockedTrackPushNotificationsBannerTap = jest
+ .spyOn(analytics, "trackPushNotificationsBannerTap")
+ .mockImplementation(_ => undefined);
const component = renderTestingComponent(
);
- fireEvent(component.getByTestId("pushNotificationsBanner"), "press");
- expect(testPressHandler).toHaveBeenCalledTimes(1);
+ fireEvent(component.getByTestId("pushNotificationsBanner"), "onPress");
+
+ expect(spyOnMockedTrackPushNotificationsBannerTap).toHaveBeenCalledTimes(1);
+ expect(spyOnMockedTrackPushNotificationsBannerTap).toHaveBeenCalledWith(
+ MESSAGES_ROUTES.MESSAGES_HOME
+ );
+ expect(
+ spyOnMockedOpenSystemNotificationSettingsScreen
+ ).toHaveBeenCalledTimes(1);
});
- it("should correctly dispatch the closeHandler", () => {
+ it("should correctly dispatch the closeHandler and call 'trackPushNotificationsBannerClosure' when the dismission count is less than two", () => {
+ const spyOnMockedTrackPushNotificationsBannerClosure = jest
+ .spyOn(analytics, "trackPushNotificationsBannerClosure")
+ .mockImplementation(constUndefined);
+ const spyOnMockedTrackPushNotificationBannerDismissAlert = jest
+ .spyOn(analytics, "trackPushNotificationBannerDismissAlert")
+ .mockImplementation(constUndefined);
const testClose = jest.fn();
const component = renderTestingComponent(
);
fireEvent(
component.getByA11yLabel(I18n.t("global.buttons.close")),
- "press"
+ "onPress"
);
+ expect(
+ spyOnMockedTrackPushNotificationsBannerClosure
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissAlert
+ ).toHaveBeenCalledTimes(0);
expect(testClose).toHaveBeenCalledTimes(1);
});
-
it.each([0, 1, 2, 3, 4, 5, 6, 7, 8])(
// dismissed should never be more than 2, but just in case
"should only open BS after third banner dismissal (current: %p )",
@@ -62,6 +94,9 @@ describe("PushNotificationsBanner", () => {
"timesPushNotificationBannerDismissedSelector"
)
.mockImplementation(() => timeDismissed);
+ const spyOnMockedTrackPushNotificationBannerDismissAlert = jest
+ .spyOn(analytics, "trackPushNotificationBannerDismissAlert")
+ .mockImplementation(constUndefined);
jest.spyOn(BS, "useIOBottomSheetModal").mockImplementation(_ => ({
bottomSheet: <>>,
dismiss: jest.fn(),
@@ -72,9 +107,15 @@ describe("PushNotificationsBanner", () => {
);
fireEvent(
component.getByA11yLabel(I18n.t("global.buttons.close")),
- "press"
+ "onPress"
+ );
+ const dismissionThresholdReached = timeDismissed >= 2;
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissAlert
+ ).toHaveBeenCalledTimes(dismissionThresholdReached ? 1 : 0);
+ expect(testPresentBS).toHaveBeenCalledTimes(
+ dismissionThresholdReached ? 1 : 0
);
- expect(testPresentBS).toHaveBeenCalledTimes(timeDismissed >= 2 ? 1 : 0);
}
);
it('should reset the dismiss state if "shouldResetNotificationBannerDismissStateSelector" evaluates to "true" ', () => {
@@ -88,16 +129,91 @@ describe("PushNotificationsBanner", () => {
jest
.spyOn(IOHOOKS, "useIODispatch")
.mockImplementation(() => ioDispatchMock);
+ const spyOnMockedTrackPushNotificationBannerForceShow = jest
+ .spyOn(analytics, "trackPushNotificationBannerForceShow")
+ .mockImplementation(constUndefined);
const component = renderTestingComponent(
null} />
);
expect(component).not.toBeNull();
+
+ expect(
+ spyOnMockedTrackPushNotificationBannerForceShow
+ ).toHaveBeenCalledTimes(1);
expect(ioDispatchMock).toHaveBeenCalledWith(
ACTIONS.resetNotificationBannerDismissState()
);
});
+ it("bottom sheet primary action should call 'trackPushNotificationBannerDismissOutcome' and the close handler", () => {
+ const spyOnBS = jest.spyOn(BS, "useIOBottomSheetModal");
+ const spyOnMockedTrackPushNotificationBannerDismissOutcome = jest
+ .spyOn(analytics, "trackPushNotificationBannerDismissOutcome")
+ .mockImplementation(_ => undefined);
+ const mockCloseHandler = jest.fn();
+ renderTestingComponent(
+
+ );
+
+ expect(spyOnBS.mock.calls.length).toBe(1);
+ expect(spyOnBS.mock.calls[0].length).toBe(1);
+ expect(spyOnBS.mock.calls[0][0]).toBeDefined();
+
+ const primaryActionCallback =
+ spyOnBS.mock.calls[0][0].footer?.props.actions.primary.onPress;
+ expect(primaryActionCallback).toBeDefined();
+ expect(typeof primaryActionCallback).toBe("function");
+
+ primaryActionCallback();
+
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissOutcome
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissOutcome
+ ).toHaveBeenCalledWith("remind_later");
+
+ expect(mockCloseHandler).toHaveBeenCalledTimes(1);
+ });
+ it("bottom sheet secondary action should call 'trackPushNotificationBannerDismissOutcome' and dispatch 'setPushNotificationBannerForceDismissed'", () => {
+ const mockedDispatch = jest.fn();
+ jest
+ .spyOn(IOHOOKS, "useIODispatch")
+ .mockImplementation(() => mockedDispatch);
+ const spyOnBS = jest.spyOn(BS, "useIOBottomSheetModal");
+ const spyOnMockedTrackPushNotificationBannerDismissOutcome = jest
+ .spyOn(analytics, "trackPushNotificationBannerDismissOutcome")
+ .mockImplementation(_ => undefined);
+ const mockCloseHandler = jest.fn();
+
+ renderTestingComponent(
+
+ );
+
+ expect(spyOnBS.mock.calls.length).toBe(1);
+ expect(spyOnBS.mock.calls[0].length).toBe(1);
+ expect(spyOnBS.mock.calls[0][0]).toBeDefined();
+
+ const secondaryActionCallback =
+ spyOnBS.mock.calls[0][0].footer?.props.actions.secondary.onPress;
+ expect(secondaryActionCallback).toBeDefined();
+ expect(typeof secondaryActionCallback).toBe("function");
+
+ secondaryActionCallback();
+
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissOutcome
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockedTrackPushNotificationBannerDismissOutcome
+ ).toHaveBeenCalledWith("deactivate");
+
+ expect(mockedDispatch).toHaveBeenCalledTimes(1);
+ expect(mockedDispatch).toHaveBeenCalledWith(
+ setPushNotificationBannerForceDismissed()
+ );
+ });
});
const renderTestingComponent = (component: React.ReactElement) => {
diff --git a/ts/features/pushNotifications/components/__tests__/__snapshots__/PushNotificationsBanner.test.tsx.snap b/ts/features/pushNotifications/components/__tests__/__snapshots__/PushNotificationsBanner.test.tsx.snap
index 80e7332c575..9833d6d6123 100644
--- a/ts/features/pushNotifications/components/__tests__/__snapshots__/PushNotificationsBanner.test.tsx.snap
+++ b/ts/features/pushNotifications/components/__tests__/__snapshots__/PushNotificationsBanner.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`PushNotificationsBanner should render correctly 1`] = `
+exports[`PushNotificationsBanner should render correctly and call 'trackPushNotificationsBannerVisualized' 1`] = `
{
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+ it("should not call 'trackPushNotificationBannerStillHidden' if the banner has not been forced dismissed", () => {
+ jest
+ .spyOn(selectors, "isForceDismissAndNotUnreadMessagesHiddenSelector")
+ .mockReturnValue(false);
+ jest
+ .spyOn(selectors, "unreadMessagesCountAfterForceDismissionSelector")
+ .mockReturnValue(15);
+ const mockTrackPushNotificationBannerStillHidden = jest
+ .spyOn(analytics, "trackPushNotificationBannerStillHidden")
+ .mockImplementation(_unreadMessageCount => undefined);
+
+ renderScreen();
+
+ expect(mockTrackPushNotificationBannerStillHidden.mock.calls.length).toBe(
+ 0
+ );
+ });
+ it("should not call 'trackPushNotificationBannerStillHidden' if data about unread messages is not available yet", () => {
+ jest
+ .spyOn(selectors, "isForceDismissAndNotUnreadMessagesHiddenSelector")
+ .mockReturnValue(true);
+ jest
+ .spyOn(selectors, "unreadMessagesCountAfterForceDismissionSelector")
+ .mockReturnValue(undefined);
+ const mockTrackPushNotificationBannerStillHidden = jest
+ .spyOn(analytics, "trackPushNotificationBannerStillHidden")
+ .mockImplementation(_unreadMessageCount => undefined);
+
+ renderScreen();
+
+ expect(mockTrackPushNotificationBannerStillHidden.mock.calls.length).toBe(
+ 0
+ );
+ });
+ it("should call 'trackPushNotificationBannerStillHidden' with proper parameters", () => {
+ jest
+ .spyOn(selectors, "isForceDismissAndNotUnreadMessagesHiddenSelector")
+ .mockReturnValue(true);
+ jest
+ .spyOn(selectors, "unreadMessagesCountAfterForceDismissionSelector")
+ .mockReturnValue(15);
+ const mockTrackPushNotificationBannerStillHidden = jest
+ .spyOn(analytics, "trackPushNotificationBannerStillHidden")
+ .mockImplementation(_unreadMessageCount => undefined);
+
+ renderScreen();
+
+ expect(mockTrackPushNotificationBannerStillHidden.mock.calls.length).toBe(
+ 1
+ );
+ expect(
+ mockTrackPushNotificationBannerStillHidden.mock.calls[0].length
+ ).toBe(1);
+ expect(mockTrackPushNotificationBannerStillHidden.mock.calls[0][0]).toBe(
+ 15
+ );
+ });
+});
+
+const renderScreen = () => {
+ const initialState = appReducer(undefined, applicationChangeState("active"));
+ const store = createStore(appReducer, initialState as any);
+
+ return renderScreenWithNavigationStoreContext(
+ HookWrapper,
+ MESSAGES_ROUTES.MESSAGES_HOME,
+ {},
+ store
+ );
+};
+
+const HookWrapper = () => {
+ usePushNotificationsBannerTracking();
+ return undefined;
+};
diff --git a/ts/features/pushNotifications/hooks/usePushNotificationsBannerTracking.tsx b/ts/features/pushNotifications/hooks/usePushNotificationsBannerTracking.tsx
new file mode 100644
index 00000000000..68a16c86f06
--- /dev/null
+++ b/ts/features/pushNotifications/hooks/usePushNotificationsBannerTracking.tsx
@@ -0,0 +1,26 @@
+import { useEffect } from "react";
+import { useIOSelector, useIOStore } from "../../../store/hooks";
+import {
+ isForceDismissAndNotUnreadMessagesHiddenSelector,
+ unreadMessagesCountAfterForceDismissionSelector
+} from "../store/selectors/notificationsBannerDismissed";
+import { trackPushNotificationBannerStillHidden } from "../analytics";
+
+export const usePushNotificationsBannerTracking = () => {
+ const store = useIOStore();
+ const isBannerForceDismissHidden = useIOSelector(
+ isForceDismissAndNotUnreadMessagesHiddenSelector
+ );
+
+ useEffect(() => {
+ if (isBannerForceDismissHidden) {
+ const unreadMessageCountMaybe =
+ unreadMessagesCountAfterForceDismissionSelector(store.getState());
+ if (unreadMessageCountMaybe != null) {
+ trackPushNotificationBannerStillHidden(unreadMessageCountMaybe);
+ }
+ }
+ }, [isBannerForceDismissHidden, store]);
+
+ return undefined;
+};
diff --git a/ts/features/pushNotifications/sagas/__tests__/profileAndSystemNotificationsPermissions.test.tsx b/ts/features/pushNotifications/sagas/__tests__/profileAndSystemNotificationsPermissions.test.tsx
index 93d8c761975..1944dfc5bb7 100644
--- a/ts/features/pushNotifications/sagas/__tests__/profileAndSystemNotificationsPermissions.test.tsx
+++ b/ts/features/pushNotifications/sagas/__tests__/profileAndSystemNotificationsPermissions.test.tsx
@@ -12,7 +12,8 @@ import { PushNotificationsContentTypeEnum } from "../../../../../definitions/bac
import { ReminderStatusEnum } from "../../../../../definitions/backend/ReminderStatus";
import {
trackNotificationsOptInPreviewStatus,
- trackNotificationsOptInReminderStatus
+ trackNotificationsOptInReminderStatus,
+ trackPushNotificationSystemPopupShown
} from "../../analytics";
import { updateMixpanelSuperProperties } from "../../../../mixpanelConfig/superProperties";
import { updateMixpanelProfileProperties } from "../../../../mixpanelConfig/profileProperties";
@@ -21,6 +22,7 @@ import {
updateNotificationPermissionsIfNeeded
} from "../common";
import { setPushPermissionsRequestDuration } from "../../store/actions/environment";
+import { hasUserSeenSystemNotificationsPromptSelector } from "../../store/selectors";
const generateUserProfile = (
hasDoneNotificationOptIn: boolean,
@@ -83,8 +85,12 @@ describe("checkNotificationsPreferencesSaga", () => {
.call(requestNotificationPermissions)
.next(true)
.call(performance.now)
- .next(10)
- .put(setPushPermissionsRequestDuration(10))
+ .next(1000)
+ .put(setPushPermissionsRequestDuration(1000))
+ .next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(true)
+ .call(trackPushNotificationSystemPopupShown)
.next()
.call(updateNotificationPermissionsIfNeeded, true)
.next()
@@ -134,6 +140,8 @@ describe("checkNotificationsPreferencesSaga", () => {
.next(10)
.put(setPushPermissionsRequestDuration(10))
.next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(false)
.call(updateNotificationPermissionsIfNeeded, false)
.next()
.call(
@@ -207,8 +215,12 @@ describe("checkNotificationsPreferencesSaga", () => {
.call(requestNotificationPermissions)
.next(true)
.call(performance.now)
- .next(10)
- .put(setPushPermissionsRequestDuration(10))
+ .next(1000)
+ .put(setPushPermissionsRequestDuration(1000))
+ .next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(true)
+ .call(trackPushNotificationSystemPopupShown)
.next()
.call(updateNotificationPermissionsIfNeeded, true)
.next()
@@ -235,6 +247,8 @@ describe("checkNotificationsPreferencesSaga", () => {
.next(10)
.put(setPushPermissionsRequestDuration(10))
.next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(false)
.call(updateNotificationPermissionsIfNeeded, false)
.next()
.select()
@@ -293,8 +307,12 @@ describe("checkNotificationsPreferencesSaga", () => {
.call(requestNotificationPermissions)
.next(true)
.call(performance.now)
- .next(10)
- .put(setPushPermissionsRequestDuration(10))
+ .next(1000)
+ .put(setPushPermissionsRequestDuration(1000))
+ .next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(true)
+ .call(trackPushNotificationSystemPopupShown)
.next()
.call(updateNotificationPermissionsIfNeeded, true)
.next()
@@ -344,6 +362,8 @@ describe("checkNotificationsPreferencesSaga", () => {
.next(10)
.put(setPushPermissionsRequestDuration(10))
.next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(false)
.call(updateNotificationPermissionsIfNeeded, false)
.next()
.call(
@@ -417,8 +437,12 @@ describe("checkNotificationsPreferencesSaga", () => {
.call(requestNotificationPermissions)
.next(true)
.call(performance.now)
- .next(10)
- .put(setPushPermissionsRequestDuration(10))
+ .next(1000)
+ .put(setPushPermissionsRequestDuration(1000))
+ .next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(true)
+ .call(trackPushNotificationSystemPopupShown)
.next()
.call(updateNotificationPermissionsIfNeeded, true)
.next()
@@ -445,6 +469,8 @@ describe("checkNotificationsPreferencesSaga", () => {
.next(10)
.put(setPushPermissionsRequestDuration(10))
.next()
+ .select(hasUserSeenSystemNotificationsPromptSelector)
+ .next(false)
.call(updateNotificationPermissionsIfNeeded, false)
.next()
.select()
diff --git a/ts/features/pushNotifications/sagas/profileAndSystemNotificationsPermissions.ts b/ts/features/pushNotifications/sagas/profileAndSystemNotificationsPermissions.ts
index ebe31d93988..44599f031c1 100644
--- a/ts/features/pushNotifications/sagas/profileAndSystemNotificationsPermissions.ts
+++ b/ts/features/pushNotifications/sagas/profileAndSystemNotificationsPermissions.ts
@@ -11,11 +11,13 @@ import { isProfileFirstOnBoarding } from "../../../store/reducers/profile";
import { GlobalState } from "../../../store/reducers/types";
import {
trackNotificationsOptInPreviewStatus,
- trackNotificationsOptInReminderStatus
+ trackNotificationsOptInReminderStatus,
+ trackPushNotificationSystemPopupShown
} from "../analytics";
import { setPushPermissionsRequestDuration } from "../store/actions/environment";
import { notificationsInfoScreenConsent } from "../store/actions/profileNotificationPermissions";
import { requestNotificationPermissions } from "../utils";
+import { hasUserSeenSystemNotificationsPromptSelector } from "../store/selectors";
import {
checkAndUpdateNotificationPermissionsIfNeeded,
updateNotificationPermissionsIfNeeded
@@ -76,6 +78,13 @@ export function* profileAndSystemNotificationsPermissions(
const requestDuration = endRequestTime - startRequestTime;
yield* put(setPushPermissionsRequestDuration(requestDuration));
+ const systemPermissionPromptShown = yield* select(
+ hasUserSeenSystemNotificationsPromptSelector
+ );
+ if (systemPermissionPromptShown) {
+ yield* call(trackPushNotificationSystemPopupShown);
+ }
+
yield* call(
updateNotificationPermissionsIfNeeded,
userHasGivenNotificationPermission
diff --git a/ts/features/pushNotifications/store/selectors/__tests__/notificationsBannerDismissed.test.ts b/ts/features/pushNotifications/store/selectors/__tests__/notificationsBannerDismissed.test.ts
index e80a3d48610..798e4c4a8f2 100644
--- a/ts/features/pushNotifications/store/selectors/__tests__/notificationsBannerDismissed.test.ts
+++ b/ts/features/pushNotifications/store/selectors/__tests__/notificationsBannerDismissed.test.ts
@@ -3,8 +3,11 @@ import { GlobalState } from "../../../../../store/reducers/types";
import * as ALL_PAGINATED from "../../../../messages/store/reducers/allPaginated";
import { UIMessage } from "../../../../messages/types";
import {
+ isForceDismissAndNotUnreadMessagesHiddenSelector,
+ pushNotificationsBannerForceDismissionDateSelector,
shouldResetNotificationBannerDismissStateSelector,
- timesPushNotificationBannerDismissedSelector
+ timesPushNotificationBannerDismissedSelector,
+ unreadMessagesCountAfterForceDismissionSelector
} from "../notificationsBannerDismissed";
import { UserBehaviourState } from "../../reducers/userBehaviour";
@@ -48,6 +51,22 @@ const readMessage = {
createdAt: new Date("2100-01-01")
} as unknown as UIMessage;
+describe("pushNotificationsBannerForceDismissionDateSelector", () => {
+ it("should return undefined when 'pushNotificationBannerForceDismissionDate' has no value", () => {
+ const dismissionDate = pushNotificationsBannerForceDismissionDateSelector(
+ getTestState({})
+ );
+ expect(dismissionDate).toBe(undefined);
+ });
+ it("should return proper value when 'pushNotificationBannerForceDismissionDate' is defined", () => {
+ const dismissionDateUE = new Date().getTime();
+ const dismissionDate = pushNotificationsBannerForceDismissionDateSelector(
+ getTestState({ forceDismissionDate: dismissionDateUE })
+ );
+ expect(dismissionDate).toBe(dismissionDateUE);
+ });
+});
+
describe("timesPushNotificationsBannerDismissedSelector", () => {
it("should return timesPushNotificationsBannerDismissed", () => {
expect(
@@ -58,37 +77,76 @@ describe("timesPushNotificationsBannerDismissedSelector", () => {
});
});
-describe("shouldResetNotificationsBannerDismissStateSelector", () => {
+describe("unreadMessagesCountAfterForceDismissionSelector, isForceDismissAndNotUnreadMessagesHiddenSelector, shouldResetNotificationBannerDismissStateSelector", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});
- it("should return false if 'messageList' is undefined", () => {
- expect(
- shouldResetNotificationBannerDismissStateSelector(getTestState({}))
- ).toBe(false);
+ describe(`if 'messageList' is undefined`, () => {
+ const noMessageListTestState = getTestState({
+ forceDismissionDate: 1,
+ timesDismissed: 3
+ });
+ it("unreadMessagesCountAfterForceDismissionSelector should return 'undefined'", () => {
+ expect(
+ unreadMessagesCountAfterForceDismissionSelector(noMessageListTestState)
+ ).toBeUndefined();
+ });
+ it("isForceDismissAndNotUnreadMessagesHiddenSelector should return 'false'", () => {
+ expect(
+ isForceDismissAndNotUnreadMessagesHiddenSelector(noMessageListTestState)
+ ).toBe(false);
+ });
+ it("shouldResetNotificationBannerDismissStateSelector should return 'false'", () => {
+ expect(
+ shouldResetNotificationBannerDismissStateSelector(
+ noMessageListTestState
+ )
+ ).toBe(false);
+ });
});
- it("should return false if 'forceDismissDate' is undefined", () => {
- expect(
- shouldResetNotificationBannerDismissStateSelector(
- getTestState({ forceDismissionDate: undefined })
- )
- ).toBe(false);
+ describe("if 'forceDismissDate' is 'undefined'", () => {
+ const noForceDismissDateTestState = getTestState({
+ messages: [unreadMessage, unreadMessage, unreadMessage, unreadMessage],
+ timesDismissed: 3
+ });
+ it("unreadMessagesCountAfterForceDismissionSelector should return 'undefined'", () => {
+ expect(
+ unreadMessagesCountAfterForceDismissionSelector(
+ noForceDismissDateTestState
+ )
+ ).toBeUndefined();
+ });
+ it("isForceDismissAndNotUnreadMessagesHiddenSelector should return 'false'", () => {
+ expect(
+ isForceDismissAndNotUnreadMessagesHiddenSelector(
+ noForceDismissDateTestState
+ )
+ ).toBe(false);
+ });
+ it("shouldResetNotificationBannerDismissStateSelector should return 'false' ", () => {
+ expect(
+ shouldResetNotificationBannerDismissStateSelector(
+ noForceDismissDateTestState
+ )
+ ).toBe(false);
+ });
});
- it.each`
- unread | moreThanFour | isNew | expected
- ${true} | ${true} | ${true} | ${true}
- ${true} | ${true} | ${false} | ${false}
- ${true} | ${false} | ${true} | ${false}
- ${true} | ${false} | ${false} | ${false}
- ${false} | ${true} | ${true} | ${false}
- ${false} | ${true} | ${false} | ${false}
- ${false} | ${false} | ${true} | ${false}
- ${false} | ${false} | ${false} | ${false}
+ describe.each`
+ unread | moreThanFour | isNew | unreadCount | expected
+ ${true} | ${true} | ${true} | ${5} | ${true}
+ ${true} | ${true} | ${false} | ${0} | ${false}
+ ${true} | ${false} | ${true} | ${2} | ${false}
+ ${true} | ${false} | ${false} | ${0} | ${false}
+ ${false} | ${true} | ${true} | ${0} | ${false}
+ ${false} | ${true} | ${false} | ${0} | ${false}
+ ${false} | ${false} | ${true} | ${0} | ${false}
+ ${false} | ${false} | ${false} | ${0} | ${false}
`(
- "should return $expected when messageListForCategorySelector returns a list with {more than 4? $moreThanFour, unread? $unread, new? $isNew} messages",
- ({ unread, moreThanFour, isNew, expected }) => {
+ `when 'messageListForCategorySelector' returns a list with {more than 4? $moreThanFour, unread? $unread, unreadCount? $unreadCount new? $isNew} messages`,
+ // `should return '$unreadCount' '!$expected' '$expected' when messageListForCategorySelector returns a list with {more than 4? $moreThanFour, unread? $unread, unreadCount? $unreadCount new? $isNew} messages`,
+ ({ unread, moreThanFour, isNew, unreadCount, expected }) => {
const messageList = unread
? [
unreadMessage,
@@ -106,16 +164,27 @@ describe("shouldResetNotificationsBannerDismissStateSelector", () => {
jest
.spyOn(ALL_PAGINATED, "messageListForCategorySelector")
.mockImplementation(() => messageList);
- expect(
- shouldResetNotificationBannerDismissStateSelector(
- getTestState({
- messages: messageList,
- forceDismissionDate: isNew
- ? new Date("2000-1-1").getTime()
- : new Date("2500-1-1").getTime()
- })
- )
- ).toBe(expected);
+ const testState = getTestState({
+ messages: messageList,
+ forceDismissionDate: isNew
+ ? new Date("2000-1-1").getTime()
+ : new Date("2500-1-1").getTime()
+ });
+ it(`unreadMessagesCountAfterForceDismissionSelector should return '${unreadCount}'`, () => {
+ expect(unreadMessagesCountAfterForceDismissionSelector(testState)).toBe(
+ unreadCount
+ );
+ });
+ it(`isForceDismissAndNotUnreadMessagesHiddenSelector should return '${!expected}'`, () => {
+ expect(
+ isForceDismissAndNotUnreadMessagesHiddenSelector(testState)
+ ).toBe(!expected);
+ });
+ it(`shouldResetNotificationBannerDismissStateSelector should return '${expected}'`, () => {
+ expect(
+ shouldResetNotificationBannerDismissStateSelector(testState)
+ ).toBe(expected);
+ });
}
);
});
diff --git a/ts/features/pushNotifications/store/selectors/notificationsBannerDismissed.ts b/ts/features/pushNotifications/store/selectors/notificationsBannerDismissed.ts
index 93bc63a595b..7b44f579324 100644
--- a/ts/features/pushNotifications/store/selectors/notificationsBannerDismissed.ts
+++ b/ts/features/pushNotifications/store/selectors/notificationsBannerDismissed.ts
@@ -2,7 +2,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot";
import { GlobalState } from "../../../../store/reducers/types";
import { UIMessage } from "../../../messages/types";
-const NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS = 4;
+export const NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS = 4;
export const pushNotificationsBannerForceDismissionDateSelector = (
state: GlobalState
@@ -13,24 +13,52 @@ export const timesPushNotificationBannerDismissedSelector = (
state: GlobalState
) => state.notifications.userBehaviour.pushNotificationBannerDismissalCount;
-export const shouldResetNotificationBannerDismissStateSelector = (
+export const unreadMessagesCountAfterForceDismissionSelector = (
state: GlobalState
) => {
const forceDismissDate =
pushNotificationsBannerForceDismissionDateSelector(state);
+ if (forceDismissDate === undefined) {
+ return undefined;
+ }
const messagesList = pot.toUndefined(
state.entities.messages.allPaginated.inbox.data
)?.page;
-
- if (messagesList === undefined || forceDismissDate === undefined) {
- return false;
+ if (!messagesList) {
+ return undefined;
}
- const newUnreadCount = messagesList.filter(
+ return messagesList.filter(
(message: UIMessage) =>
message.createdAt.getTime() > forceDismissDate && !message.isRead
).length;
+};
+
+export const isForceDismissAndNotUnreadMessagesHiddenSelector = (
+ state: GlobalState
+) => {
+ const unreadMessageCountAfterForceDismissionMaybe =
+ unreadMessagesCountAfterForceDismissionSelector(state);
+ if (unreadMessageCountAfterForceDismissionMaybe == null) {
+ return false;
+ }
+ return (
+ unreadMessageCountAfterForceDismissionMaybe <
+ NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS
+ );
+};
- return newUnreadCount >= NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS;
+export const shouldResetNotificationBannerDismissStateSelector = (
+ state: GlobalState
+) => {
+ const unreadMessageCountAfterForceDismissionMaybe =
+ unreadMessagesCountAfterForceDismissionSelector(state);
+ if (unreadMessageCountAfterForceDismissionMaybe == null) {
+ return false;
+ }
+ return (
+ unreadMessageCountAfterForceDismissionMaybe >=
+ NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS
+ );
};
diff --git a/ts/screens/profile/ProfileMainScreenTopBanner.tsx b/ts/screens/profile/ProfileMainScreenTopBanner.tsx
index 907397b5ff0..25d88fb1672 100644
--- a/ts/screens/profile/ProfileMainScreenTopBanner.tsx
+++ b/ts/screens/profile/ProfileMainScreenTopBanner.tsx
@@ -4,7 +4,7 @@ import {
ContentWrapper,
VSpacer
} from "@pagopa/io-app-design-system";
-import React, { useCallback } from "react";
+import React, { useCallback, useEffect } from "react";
import { setShowProfileBanner } from "../../features/profileSettings/store/actions";
import { profileBannerToShowSelector } from "../../features/profileSettings/store/selectors";
import { openSystemNotificationSettingsScreen } from "../../features/pushNotifications/utils";
@@ -12,6 +12,10 @@ import I18n from "../../i18n";
import { useIONavigation } from "../../navigation/params/AppParamsList";
import ROUTES from "../../navigation/routes";
import { useIODispatch, useIOSelector } from "../../store/hooks";
+import {
+ trackPushNotificationsBannerTap,
+ trackPushNotificationsBannerVisualized
+} from "../../features/pushNotifications/analytics";
export const ProfileMainScreenTopBanner = () => {
const bannerToShow = useIOSelector(profileBannerToShowSelector);
@@ -30,6 +34,17 @@ export const ProfileMainScreenTopBanner = () => {
[navigation]
);
+ const onPressNotifications = React.useCallback(() => {
+ trackPushNotificationsBannerTap(ROUTES.SETTINGS_MAIN);
+ openSystemNotificationSettingsScreen();
+ }, []);
+
+ useEffect(() => {
+ if (bannerToShow === "NOTIFICATIONS") {
+ trackPushNotificationsBannerVisualized(ROUTES.SETTINGS_MAIN);
+ }
+ }, [bannerToShow]);
+
switch (bannerToShow) {
case "NOTIFICATIONS":
return (
@@ -38,7 +53,7 @@ export const ProfileMainScreenTopBanner = () => {
diff --git a/ts/screens/profile/__test__/ProfileMainScreenTopBanner.test.tsx b/ts/screens/profile/__test__/ProfileMainScreenTopBanner.test.tsx
index 2cdb678210e..6cec2480ba5 100644
--- a/ts/screens/profile/__test__/ProfileMainScreenTopBanner.test.tsx
+++ b/ts/screens/profile/__test__/ProfileMainScreenTopBanner.test.tsx
@@ -1,6 +1,7 @@
-import { fireEvent } from "@testing-library/react-native";
-import React from "react";
-import { renderComponent } from "../../../components/__tests__/ForceScrollDownView.test";
+import { fireEvent, render } from "@testing-library/react-native";
+import React, { PropsWithChildren } from "react";
+import { createStore } from "redux";
+import { Provider } from "react-redux";
import * as profileBannerImport from "../../../features/profileSettings/store/selectors";
import { GlobalState } from "../../../store/reducers/types";
import { ProfileMainScreenTopBanner } from "../ProfileMainScreenTopBanner";
@@ -9,6 +10,9 @@ import ROUTES from "../../../navigation/routes";
import TypedI18n from "../../../i18n";
import { setShowProfileBanner } from "../../../features/profileSettings/store/actions";
import { mockAccessibilityInfo } from "../../../utils/testAccessibility";
+import * as analytics from "../../../features/pushNotifications/analytics";
+import { appReducer } from "../../../store/reducers";
+import { applicationChangeState } from "../../../store/actions/application";
jest.spyOn(settingsNavigate, "openSystemNotificationSettingsScreen");
const mockNavigate = jest.fn();
@@ -62,6 +66,9 @@ describe("ProfileMainScreenTopBanner", () => {
jest
.spyOn(profileBannerImport, "profileBannerToShowSelector")
.mockImplementation((_: GlobalState) => "NOTIFICATIONS");
+ const spyOnMockTrackPushNotificationsBannerVisualized = jest
+ .spyOn(analytics, "trackPushNotificationsBannerVisualized")
+ .mockImplementation(_ => undefined);
const root = renderComponent();
const component = root.getByTestId("notifications-banner");
@@ -70,6 +77,12 @@ describe("ProfileMainScreenTopBanner", () => {
fireEvent.press(component);
+ expect(
+ spyOnMockTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledWith(ROUTES.SETTINGS_MAIN);
expect(
settingsNavigate.openSystemNotificationSettingsScreen
).toHaveBeenCalled();
@@ -106,4 +119,44 @@ describe("ProfileMainScreenTopBanner", () => {
expect(mockDispatch).toHaveBeenCalledWith(setShowProfileBanner(false));
});
+ it(`should call 'trackPushNotificationsBannerVisualized' on first rendering if the push notification banner is the one to show`, () => {
+ jest
+ .spyOn(profileBannerImport, "profileBannerToShowSelector")
+ .mockImplementation((_: GlobalState) => "NOTIFICATIONS");
+ const spyOnMockTrackPushNotificationsBannerVisualized = jest
+ .spyOn(analytics, "trackPushNotificationsBannerVisualized")
+ .mockImplementation(_ => undefined);
+ renderComponent();
+ expect(
+ spyOnMockTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ spyOnMockTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledWith(ROUTES.SETTINGS_MAIN);
+ });
+ it(`should not have called 'trackPushNotificationsBannerVisualized' on first rendering if the push notification banner is not the shown one`, () => {
+ jest
+ .spyOn(profileBannerImport, "profileBannerToShowSelector")
+ .mockImplementation((_: GlobalState) => "PROFILE_BANNER");
+ const spyOnMockTrackPushNotificationsBannerVisualized = jest
+ .spyOn(analytics, "trackPushNotificationsBannerVisualized")
+ .mockImplementation(_ => undefined);
+ renderComponent();
+ expect(
+ spyOnMockTrackPushNotificationsBannerVisualized
+ ).toHaveBeenCalledTimes(0);
+ });
});
+
+export const renderComponent = (component: React.ReactElement) => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+ const store = createStore(appReducer, globalState as any);
+
+ const Wrapper = ({ children }: PropsWithChildren) => (
+ {children}
+ );
+
+ return render(component, {
+ wrapper: Wrapper
+ });
+};
diff --git a/ts/screens/profile/analytics/__test__/index.test.ts b/ts/screens/profile/analytics/__test__/index.test.ts
new file mode 100644
index 00000000000..0ebc5a430ea
--- /dev/null
+++ b/ts/screens/profile/analytics/__test__/index.test.ts
@@ -0,0 +1,65 @@
+import * as Mixpanel from "../../../../mixpanel";
+import {
+ trackSettingsDiscoverBannerClosure,
+ trackSettingsDiscoverBannerTap,
+ trackSettingsDiscoverBannerVisualized
+} from "..";
+
+describe("index", () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+ it(`'trackSettingsDiscoverBannerVisualized' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackSettingsDiscoverBannerVisualized();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "screen_view",
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: "MESSAGES_HOME",
+ banner_landing: "SETTINGS_MAIN"
+ });
+ });
+ it(`'trackSettingsDiscoverBannerClosure' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackSettingsDiscoverBannerTap();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("TAP_BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "action",
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: "MESSAGES_HOME",
+ banner_landing: "SETTINGS_MAIN"
+ });
+ });
+ it(`'trackSettingsDiscoverBannerTap' should have expected event name and properties`, () => {
+ const mockMixpanelTrack = getMockMixpanelTrack();
+
+ void trackSettingsDiscoverBannerClosure();
+
+ expect(mockMixpanelTrack.mock.calls.length).toBe(1);
+ expect(mockMixpanelTrack.mock.calls[0].length).toBe(2);
+ expect(mockMixpanelTrack.mock.calls[0][0]).toBe("CLOSE_BANNER");
+ expect(mockMixpanelTrack.mock.calls[0][1]).toEqual({
+ event_category: "UX",
+ event_type: "action",
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: "MESSAGES_HOME",
+ banner_landing: "SETTINGS_MAIN"
+ });
+ });
+});
+
+const getMockMixpanelTrack = () =>
+ jest
+ .spyOn(Mixpanel, "mixpanelTrack")
+ .mockImplementation((_event, _properties) => undefined);
diff --git a/ts/screens/profile/analytics/index.ts b/ts/screens/profile/analytics/index.ts
index 4d3a2374fe3..a05026a7250 100644
--- a/ts/screens/profile/analytics/index.ts
+++ b/ts/screens/profile/analytics/index.ts
@@ -1,7 +1,9 @@
import { ServicesPreferencesModeEnum } from "../../../../definitions/backend/ServicesPreferencesMode";
+import { MESSAGES_ROUTES } from "../../../features/messages/navigation/routes";
import { mixpanelTrack } from "../../../mixpanel";
import { updateMixpanelProfileProperties } from "../../../mixpanelConfig/profileProperties";
import { updateMixpanelSuperProperties } from "../../../mixpanelConfig/superProperties";
+import ROUTES from "../../../navigation/routes";
import { profileLoadSuccess } from "../../../store/actions/profile";
import { GlobalState } from "../../../store/reducers/types";
import { FlowType, buildEventProperties } from "../../../utils/analytics";
@@ -224,3 +226,33 @@ export async function trackNotificationPreferenceConfiguration(
)
);
}
+
+export function trackSettingsDiscoverBannerVisualized() {
+ const eventName = "BANNER";
+ const props = buildEventProperties("UX", "screen_view", {
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: MESSAGES_ROUTES.MESSAGES_HOME,
+ banner_landing: ROUTES.SETTINGS_MAIN
+ });
+ void mixpanelTrack(eventName, props);
+}
+
+export function trackSettingsDiscoverBannerTap() {
+ const eventName = "TAP_BANNER";
+ const props = buildEventProperties("UX", "action", {
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: MESSAGES_ROUTES.MESSAGES_HOME,
+ banner_landing: ROUTES.SETTINGS_MAIN
+ });
+ void mixpanelTrack(eventName, props);
+}
+
+export function trackSettingsDiscoverBannerClosure() {
+ const eventName = "CLOSE_BANNER";
+ const props = buildEventProperties("UX", "action", {
+ banner_id: "settingsDiscoveryBanner",
+ banner_page: MESSAGES_ROUTES.MESSAGES_HOME,
+ banner_landing: ROUTES.SETTINGS_MAIN
+ });
+ void mixpanelTrack(eventName, props);
+}
diff --git a/ts/screens/profile/components/SettingsDiscoveryBanner.tsx b/ts/screens/profile/components/SettingsDiscoveryBanner.tsx
index 03df0540852..89f522d28de 100644
--- a/ts/screens/profile/components/SettingsDiscoveryBanner.tsx
+++ b/ts/screens/profile/components/SettingsDiscoveryBanner.tsx
@@ -1,11 +1,16 @@
import { Banner, IOVisualCostants } from "@pagopa/io-app-design-system";
-import React, { createRef } from "react";
+import React, { createRef, useEffect } from "react";
import { StyleSheet, View } from "react-native";
import I18n from "../../../i18n";
import { useIONavigation } from "../../../navigation/params/AppParamsList";
import ROUTES from "../../../navigation/routes";
import { useIODispatch } from "../../../store/hooks";
import { setHasUserAcknowledgedSettingsBanner } from "../../../features/profileSettings/store/actions";
+import {
+ trackSettingsDiscoverBannerClosure,
+ trackSettingsDiscoverBannerTap,
+ trackSettingsDiscoverBannerVisualized
+} from "../analytics";
type SettingsDiscoveryBannerProps = {
handleOnClose: () => void;
@@ -22,15 +27,21 @@ export const SettingsDiscoveryBanner = ({
const navigation = useIONavigation();
const dispatch = useIODispatch();
const handleOnPress = () => {
+ trackSettingsDiscoverBannerTap();
navigation.navigate(ROUTES.PROFILE_NAVIGATOR, {
screen: ROUTES.SETTINGS_MAIN
});
};
const closeHandler = React.useCallback(() => {
+ trackSettingsDiscoverBannerClosure();
dispatch(setHasUserAcknowledgedSettingsBanner(true));
handleOnClose();
}, [dispatch, handleOnClose]);
+ useEffect(() => {
+ trackSettingsDiscoverBannerVisualized();
+ }, []);
+
return (
);
diff --git a/ts/screens/profile/components/__test__/SettingsDiscoveryBanner.test.tsx b/ts/screens/profile/components/__test__/SettingsDiscoveryBanner.test.tsx
index d51615fa64d..0535c8ca4a6 100644
--- a/ts/screens/profile/components/__test__/SettingsDiscoveryBanner.test.tsx
+++ b/ts/screens/profile/components/__test__/SettingsDiscoveryBanner.test.tsx
@@ -1,19 +1,61 @@
import * as React from "react";
import { createStore } from "redux";
+import { constUndefined } from "fp-ts/lib/function";
+import { fireEvent } from "@testing-library/react-native";
import ROUTES from "../../../../navigation/routes";
import { applicationChangeState } from "../../../../store/actions/application";
import { appReducer } from "../../../../store/reducers";
import { GlobalState } from "../../../../store/reducers/types";
import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper";
import { SettingsDiscoveryBanner } from "../SettingsDiscoveryBanner";
+import * as analytics from "../../analytics";
+import I18n from "../../../../i18n";
describe("settingsDiscoveryBanner", () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
it("should match snapshot", () => {
const component = renderComponent(
null} />
);
expect(component.toJSON()).toMatchSnapshot();
});
+ it("should have called 'trackSettingsDiscoverBannerVisualized' on first rendering", () => {
+ const spyOnMockedTrackSettingsDiscoverBannerVisualized = jest
+ .spyOn(analytics, "trackSettingsDiscoverBannerVisualized")
+ .mockImplementation(constUndefined);
+ renderComponent( null} />);
+ expect(
+ spyOnMockedTrackSettingsDiscoverBannerVisualized
+ ).toHaveBeenCalledTimes(1);
+ });
+ it("should have called 'trackSettingsDiscoverBannerTap' on first rendering", () => {
+ const spyOnMockedTrackSettingsDiscoverBannerTap = jest
+ .spyOn(analytics, "trackSettingsDiscoverBannerTap")
+ .mockImplementation(constUndefined);
+ const component = renderComponent(
+ null} />
+ );
+ const cta = component.getByTestId("settingsDiscoveryBannerCTA");
+ fireEvent(cta, "onPress");
+ expect(spyOnMockedTrackSettingsDiscoverBannerTap).toHaveBeenCalledTimes(1);
+ });
+ it("should have called 'trackSettingsDiscoverBannerClosure' on first rendering", () => {
+ const spyOnMockedTrackSettingsDiscoverBannerClosure = jest
+ .spyOn(analytics, "trackSettingsDiscoverBannerClosure")
+ .mockImplementation(constUndefined);
+ const component = renderComponent(
+ null} />
+ );
+ const closeButton = component.getByA11yLabel(
+ I18n.t("global.buttons.close")
+ );
+ fireEvent(closeButton, "onPress");
+ expect(spyOnMockedTrackSettingsDiscoverBannerClosure).toHaveBeenCalledTimes(
+ 1
+ );
+ });
});
const renderComponent = (component: React.ReactElement) => {
diff --git a/ts/screens/profile/components/__test__/__snapshots__/SettingsDiscoveryBanner.test.tsx.snap b/ts/screens/profile/components/__test__/__snapshots__/SettingsDiscoveryBanner.test.tsx.snap
index 81717fcf663..16c99497cad 100644
--- a/ts/screens/profile/components/__test__/__snapshots__/SettingsDiscoveryBanner.test.tsx.snap
+++ b/ts/screens/profile/components/__test__/__snapshots__/SettingsDiscoveryBanner.test.tsx.snap
@@ -372,6 +372,7 @@ exports[`settingsDiscoveryBanner should match snapshot 1`] = `
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
+ testID="settingsDiscoveryBannerCTA"
>