Skip to content

Commit

Permalink
addition of required logic and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
forrest57 committed Dec 4, 2024
1 parent 4136f6d commit 7228aaf
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 23 deletions.
5 changes: 5 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2936,6 +2936,11 @@ features:
title: "Don't miss any important messages!"
body: "Turn on push notifications to know when you get a message on IO."
CTA: "Turn on push notifications"
bottomSheet:
title: "Preferisci non vedere più questo avviso?"
body: "Ti ricordiamo che attivare le notifiche push può aiutarti a non perdere comunicazioni importanti e scadenze sui pagamenti."
cta: "Ricordamelo più tardi"
cta2: "Non mostrarmelo più"
attachmentDownloadFeedback: "Download in progress"
attachments: "Attachments"
loading:
Expand Down
5 changes: 5 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2936,6 +2936,11 @@ features:
title: "Non perderti i messaggi importanti"
body: "Attiva le notifiche push per sapere quando ricevi un messaggio su IO"
CTA: "Attiva notifiche push"
bottomSheet:
title: "Preferisci non vedere più questo avviso?"
body: "Ti ricordiamo che attivare le notifiche push può aiutarti a non perdere comunicazioni importanti e scadenze sui pagamenti."
cta: "Ricordamelo più tardi"
cta2: "Non mostrarmelo più"
attachmentDownloadFeedback: "Download in corso"
attachments: "Allegati"
loading:
Expand Down
17 changes: 15 additions & 2 deletions ts/boot/configureStoreAndPersistor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { configureReactotron } from "./configureRectotron";
/**
* Redux persist will migrate the store to the current version
*/
const CURRENT_REDUX_STORE_VERSION = 38;
const CURRENT_REDUX_STORE_VERSION = 39;

// see redux-persist documentation:
// https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md
Expand Down Expand Up @@ -456,7 +456,20 @@ const migrations: MigrationManifest = {
};
},
// Remove old wallets&payments feature and persisted state
"38": (state: PersistedState) => omit(state, "payments")
"38": (state: PersistedState) => omit(state, "payments"),
// Add new push notifications banner dismissal feature
"39": (state: PersistedState) =>
merge(state, {
notifications: {
userBehaviour: {
pushNotificationsBanner: {
timesDismissed: 0,
isForceDismissed: false,
forceDismissionDate: undefined
}
}
}
})
};

const isDebuggingInChrome = isDevEnv && !!window.navigator.userAgent;
Expand Down
119 changes: 101 additions & 18 deletions ts/features/pushNotifications/components/PushNotificationsBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,111 @@
import { Banner, IOVisualCostants } from "@pagopa/io-app-design-system";
import {
Banner,
Body,
FooterActions,
IOVisualCostants
} from "@pagopa/io-app-design-system";
import * as React from "react";
import { View, StyleSheet } from "react-native";
import { StyleSheet, View } from "react-native";
import I18n from "../../../i18n";
import { useIODispatch, useIOSelector } from "../../../store/hooks";
import { useIOBottomSheetModal } from "../../../utils/hooks/bottomSheet";
import {
resetNotificationBannerDismissState,
setPushNotificationBannerForceDismissed,
setUserDismissedNotificationsBanner
} from "../store/actions/userBehaviour";
import {
shouldResetNotificationBannerDismissStateSelector,
timesPushNotificationBannerDismissedSelector
} from "../store/selectors";
import { openSystemNotificationSettingsScreen } from "../utils";
type Props = {
closeHandler: () => void;
};

export const PushNotificationsBanner = ({ closeHandler }: Props) => (
<View style={styles.margins} testID="pushnotif-bannerContainer">
<Banner
testID="pushNotificationsBanner"
title={I18n.t("features.messages.pushNotifications.banner.title")}
content={I18n.t("features.messages.pushNotifications.banner.body")}
action={I18n.t("features.messages.pushNotifications.banner.CTA")}
pictogramName="notification"
color="turquoise"
size="big"
onClose={closeHandler}
labelClose={I18n.t("global.buttons.close")}
onPress={openSystemNotificationSettingsScreen}
/>
</View>
);
export const PushNotificationsBanner = ({ closeHandler }: Props) => {
const dispatch = useIODispatch();
const shouldResetDismissState = useIOSelector(
shouldResetNotificationBannerDismissStateSelector
);

React.useEffect(() => {
if (shouldResetDismissState) {
dispatch(resetNotificationBannerDismissState());
}
}, [dispatch, shouldResetDismissState]);

const dismssionCount = useIOSelector(
timesPushNotificationBannerDismissedSelector
);
const discardModal = usePushNotificationsBannerBottomSheet(closeHandler);

const onClose = () => {
if (dismssionCount >= 2) {
discardModal.present();
} else {
dispatch(setUserDismissedNotificationsBanner());
closeHandler();
}
};

return (
<View style={styles.margins} testID="pushnotif-bannerContainer">
<Banner
testID="pushNotificationsBanner"
title={I18n.t("features.messages.pushNotifications.banner.title")}
content={I18n.t("features.messages.pushNotifications.banner.body")}
action={I18n.t("features.messages.pushNotifications.banner.CTA")}
pictogramName="notification"
color="turquoise"
size="big"
onClose={onClose}
labelClose={I18n.t("global.buttons.close")}
onPress={openSystemNotificationSettingsScreen}
/>
{discardModal.bottomSheet}
</View>
);
};

const usePushNotificationsBannerBottomSheet = (
remindLaterHandler: () => void
) => {
const dispatch = useIODispatch();

const fullCloseHandler = () =>
dispatch(setPushNotificationBannerForceDismissed());
return useIOBottomSheetModal({
title: I18n.t(
"features.messages.pushNotifications.banner.bottomSheet.title"
),
footer: (
<FooterActions
actions={{
type: "TwoButtons",
primary: {
label: I18n.t(
"features.messages.pushNotifications.banner.bottomSheet.cta"
),
onPress: remindLaterHandler
},
secondary: {
label: I18n.t(
"features.messages.pushNotifications.banner.bottomSheet.cta2"
),
onPress: fullCloseHandler
}
}}
/>
),
component: (
<Body>
{I18n.t("features.messages.pushNotifications.banner.bottomSheet.body")}
</Body>
),
snapPoint: [300]
});
};
const styles = StyleSheet.create({
margins: {
marginHorizontal: IOVisualCostants.appMarginDefault,
Expand Down
15 changes: 14 additions & 1 deletion ts/features/pushNotifications/store/actions/userBehaviour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,18 @@ import { ActionType, createStandardAction } from "typesafe-actions";
export const setEngagementScreenShown = createStandardAction(
"SET_ENGAGEMENT_SCREEN_SHOWN"
)<void>();
export const setUserDismissedNotificationsBanner = createStandardAction(
"SET_USER_DISMISSED_NOTIFICATIONS_BANNER"
)<void>();
export const setPushNotificationBannerForceDismissed = createStandardAction(
"SET_PUSH_NOTIFICATION_BANNER_FORCE_DISMISSED"
)<void>();
export const resetNotificationBannerDismissState = createStandardAction(
"RESET_NOTIFICATION_BANNER_DISMISS_STATE"
)<void>();

export type UserBehaviourActions = ActionType<typeof setEngagementScreenShown>;
export type UserBehaviourActions =
| ActionType<typeof setEngagementScreenShown>
| ActionType<typeof setUserDismissedNotificationsBanner>
| ActionType<typeof resetNotificationBannerDismissState>
| ActionType<typeof setPushNotificationBannerForceDismissed>;
47 changes: 45 additions & 2 deletions ts/features/pushNotifications/store/reducers/userBehaviour.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { getType } from "typesafe-actions";
import { Action } from "../../../../store/actions/types";
import { setEngagementScreenShown } from "../actions/userBehaviour";
import {
resetNotificationBannerDismissState,
setEngagementScreenShown,
setPushNotificationBannerForceDismissed,
setUserDismissedNotificationsBanner
} from "../actions/userBehaviour";

export type UserBehaviourState = {
engagementScreenShown: boolean;
pushNotificationsBanner: {
timesDismissed: number;
isForceDismissed: boolean;
forceDismissionDate?: Date;
};
};

const INITIAL_BANNER_STATE = {
timesDismissed: 0,
isForceDismissed: false,
forceDismissionDate: undefined
};

export const INITIAL_STATE: UserBehaviourState = {
engagementScreenShown: false
engagementScreenShown: false,
pushNotificationsBanner: {
...INITIAL_BANNER_STATE
}
};

export const userBehaviourReducer = (
Expand All @@ -17,6 +36,30 @@ export const userBehaviourReducer = (
switch (action.type) {
case getType(setEngagementScreenShown):
return { ...state, engagementScreenShown: true };
case getType(setUserDismissedNotificationsBanner):
return {
...state,
pushNotificationsBanner: {
...state.pushNotificationsBanner,
timesDismissed: state.pushNotificationsBanner.timesDismissed + 1
}
};
case getType(setPushNotificationBannerForceDismissed):
return {
...state,
pushNotificationsBanner: {
...state.pushNotificationsBanner,
isForceDismissed: true,
forceDismissionDate: new Date(Date.now())
}
};
case getType(resetNotificationBannerDismissState):
return {
...state,
pushNotificationsBanner: {
...INITIAL_BANNER_STATE
}
};
}
return state;
};
35 changes: 35 additions & 0 deletions ts/features/pushNotifications/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { GlobalState } from "../../../../store/reducers/types";
import { userFromSuccessLoginSelector } from "../../../login/info/store/selectors";
import { messageListForCategorySelector } from "../../../messages/store/reducers/allPaginated";
import { UIMessage } from "../../../messages/types";
import { areNotificationPermissionsEnabled } from "../reducers/environment";

const NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS = 4;

export const hasUserSeenSystemNotificationsPromptSelector = (
state: GlobalState
) => {
Expand All @@ -27,10 +31,40 @@ export const hasUserSeenSystemNotificationsPromptSelector = (
}
return false;
};
export const timesPushNotificationBannerDismissedSelector = (
state: GlobalState
) => state.notifications.userBehaviour.pushNotificationsBanner.timesDismissed;

export const shouldResetNotificationBannerDismissStateSelector = (
state: GlobalState
) => {
const forceDismissDate =
state.notifications.userBehaviour.pushNotificationsBanner
.forceDismissionDate;
const messagesList = messageListForCategorySelector(state, "INBOX");

if (messagesList === undefined || forceDismissDate === undefined) {
return false;
}

const newUnreadCount = messagesList.filter(
(message: UIMessage) =>
new Date(message.createdAt) > new Date(forceDismissDate) &&
!message.isRead
).length;

return newUnreadCount >= NEW_MESSAGES_COUNT_TO_RESET_FORCE_DISMISS;
};

export const isPushNotificationsBannerRenderableSelector = (
state: GlobalState
) => {
// the banner should not be renedered *only* when force dismissed and there are not enough new messages
const isForceDismissed =
state.notifications.userBehaviour.pushNotificationsBanner
.isForceDismissed &&
!shouldResetNotificationBannerDismissStateSelector(state);

const notificationsEnabled = areNotificationPermissionsEnabled(state);
// user has seen the full SPID/CIE login flow,
// so is not logged with fasLogin during this session
Expand All @@ -40,6 +74,7 @@ export const isPushNotificationsBannerRenderableSelector = (
hasUserSeenSystemNotificationsPromptSelector(state);

return (
!isForceDismissed &&
!isFullLogin &&
!notificationsEnabled &&
!hasUserSeenSystemNotificationsPrompt
Expand Down

0 comments on commit 7228aaf

Please sign in to comment.