Skip to content

Commit 8cedabc

Browse files
refactor: [IOPLT-1274] Unify Offline status alert component (#7252)
## Short description This PR aims to unify the connectivity banner handling in a single point. In this PR has been introduced a fix to prevent mixpanel event to be spammed for each user and status changes. ## List of changes proposed in this pull request - Removes the offline wrapper for ITW feature - Unify the logic to show offline banner. ## How to test Check offline feature, anything should be changed. --------- Co-authored-by: Federico Mastrini <[email protected]>
1 parent d06a5a8 commit 8cedabc

File tree

15 files changed

+353
-326
lines changed

15 files changed

+353
-326
lines changed

ts/App.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import { persistor, store } from "./boot/configureStoreAndPersistor";
1717
import { LightModalProvider } from "./components/ui/LightModal";
1818
import { sentryDsn } from "./config";
1919
import { isDevEnv } from "./utils/environment";
20-
import { StatusMessages } from "./components/StatusMessages";
20+
import { StatusMessages } from "./components/StatusMessages/StatusMessages";
2121
import { AppFeedbackProvider } from "./features/appReviews/components/AppFeedbackProvider";
22+
import { IOAlertVisibleContextProvider } from "./components/StatusMessages/IOAlertVisibleContext";
2223

2324
export type ReactNavigationInstrumentation = ReturnType<
2425
typeof Sentry.reactNavigationIntegration
@@ -125,15 +126,17 @@ const App = (): JSX.Element => (
125126
<ToastProvider>
126127
<Provider store={store}>
127128
<PersistGate loading={undefined} persistor={persistor}>
128-
<BottomSheetModalProvider>
129-
<LightModalProvider>
130-
<AppFeedbackProvider>
131-
<StatusMessages>
132-
<RootContainer store={store} />
133-
</StatusMessages>
134-
</AppFeedbackProvider>
135-
</LightModalProvider>
136-
</BottomSheetModalProvider>
129+
<IOAlertVisibleContextProvider>
130+
<BottomSheetModalProvider>
131+
<LightModalProvider>
132+
<AppFeedbackProvider>
133+
<StatusMessages>
134+
<RootContainer store={store} />
135+
</StatusMessages>
136+
</AppFeedbackProvider>
137+
</LightModalProvider>
138+
</BottomSheetModalProvider>
139+
</IOAlertVisibleContextProvider>
137140
</PersistGate>
138141
</Provider>
139142
</ToastProvider>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { PropsWithChildren, useState } from "react";
2+
3+
type IOAlertVisibleContext = {
4+
isAlertVisible: boolean;
5+
setAlertVisible: (isAlertVisible: boolean) => void;
6+
};
7+
8+
/**
9+
* Experimental Context for new UI Representations
10+
*/
11+
export const IOAlertVisibleContext = React.createContext<IOAlertVisibleContext>(
12+
{
13+
isAlertVisible: false,
14+
setAlertVisible: () => void 0
15+
}
16+
);
17+
18+
export const useIOAlertVisible = () => React.useContext(IOAlertVisibleContext);
19+
20+
type IOAlertVisibleContextProviderProps = {
21+
isNewTypefaceEnabled?: boolean;
22+
};
23+
24+
export const IOAlertVisibleContextProvider = ({
25+
children
26+
}: PropsWithChildren<IOAlertVisibleContextProviderProps>) => {
27+
const [isAlertVisible, setAlertVisible] = useState(false);
28+
return (
29+
<IOAlertVisibleContext.Provider value={{ isAlertVisible, setAlertVisible }}>
30+
{children}
31+
</IOAlertVisibleContext.Provider>
32+
);
33+
};

ts/components/StatusMessages.tsx renamed to ts/components/StatusMessages/StatusMessages.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { AlertEdgeToEdgeWrapper } from "@pagopa/io-app-design-system";
21
import { PropsWithChildren } from "react";
3-
import { useStatusAlertProps } from "../hooks/useStatusAlertProps";
2+
import { AlertEdgeToEdgeWrapper, IOColors } from "@pagopa/io-app-design-system";
3+
import { StatusBar } from "react-native";
4+
import { useStatusAlertProps } from "../../hooks/useStatusAlertProps";
45

56
type StatusMessagesProps = PropsWithChildren;
67

@@ -9,6 +10,12 @@ export const StatusMessages = ({ children }: StatusMessagesProps) => {
910

1011
return (
1112
<AlertEdgeToEdgeWrapper alertProps={statusAlert?.alertProps}>
13+
{statusAlert && (
14+
<StatusBar
15+
barStyle="dark-content"
16+
backgroundColor={IOColors["info-100"]}
17+
/>
18+
)}
1219
{children}
1320
{statusAlert?.bottomSheet}
1421
</AlertEdgeToEdgeWrapper>

ts/components/ui/IOScrollView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ import Animated, {
4444
useAnimatedStyle,
4545
useSharedValue
4646
} from "react-native-reanimated";
47-
import { useStatusAlertProps } from "../../hooks/useStatusAlertProps";
4847
import { WithTestID } from "../../types/WithTestID";
4948
import { useFooterActionsMargin } from "../../hooks/useFooterActionsMargin";
49+
import { useIOAlertVisible } from "../StatusMessages/IOAlertVisibleContext";
5050

5151
type ButtonBlockProps = Omit<
5252
IOButtonBlockSpecificProps,
@@ -169,7 +169,7 @@ export const IOScrollView = ({
169169
contentContainerStyle,
170170
testID
171171
}: IOScrollView) => {
172-
const alertProps = useStatusAlertProps();
172+
const { isAlertVisible } = useIOAlertVisible();
173173
const theme = useIOTheme();
174174

175175
/* Navigation */
@@ -248,11 +248,11 @@ export const IOScrollView = ({
248248
}));
249249

250250
const ignoreSafeAreaMargin = useMemo(() => {
251-
if (alertProps !== undefined) {
251+
if (isAlertVisible) {
252252
return true;
253253
}
254254
return headerConfig?.ignoreSafeAreaMargin;
255-
}, [headerConfig?.ignoreSafeAreaMargin, alertProps]);
255+
}, [headerConfig?.ignoreSafeAreaMargin, isAlertVisible]);
256256

257257
/* Set custom header with `react-navigation` library using
258258
`useLayoutEffect` hook */

ts/components/ui/IOScrollViewWithReveal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import Animated, {
3737
withTiming
3838
} from "react-native-reanimated";
3939
import { useFooterActionsMargin } from "../../hooks/useFooterActionsMargin";
40-
import { useStatusAlertProps } from "../../hooks/useStatusAlertProps";
4140
import { WithTestID } from "../../types/WithTestID";
41+
import { useIOAlertVisible } from "../StatusMessages/IOAlertVisibleContext";
4242

4343
type ButtonBlockProps = Omit<
4444
IOButtonBlockSpecificProps,
@@ -116,7 +116,7 @@ export const IOScrollViewWithReveal = ({
116116
hideAnchorAction,
117117
testID
118118
}: IOScrollViewWithRevealProps) => {
119-
const alertProps = useStatusAlertProps();
119+
const { isAlertVisible } = useIOAlertVisible();
120120
const theme = useIOTheme();
121121

122122
/* Navigation */
@@ -184,11 +184,11 @@ export const IOScrollViewWithReveal = ({
184184
}));
185185

186186
const ignoreSafeAreaMargin = useMemo(() => {
187-
if (alertProps !== undefined) {
187+
if (isAlertVisible) {
188188
return true;
189189
}
190190
return headerConfig?.ignoreSafeAreaMargin;
191-
}, [headerConfig?.ignoreSafeAreaMargin, alertProps]);
191+
}, [headerConfig?.ignoreSafeAreaMargin, isAlertVisible]);
192192

193193
/* Set custom header with `react-navigation` library using
194194
`useLayoutEffect` hook */

ts/features/authentication/login/cie/screens/__test__/ActivateNfcScreen.test.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,19 @@ jest.mock("../../../../../../store/hooks", () => ({
2323
useIOStore: jest.fn()
2424
}));
2525

26-
jest.mock("../../../../../../hooks/useStatusAlertProps", () => ({
27-
useStatusAlertProps: jest.fn()
28-
}));
26+
// Mock the useIOAlertVisible hook
27+
jest.mock(
28+
"../../../../../../components/StatusMessages/IOAlertVisibleContext",
29+
() => ({
30+
...jest.requireActual(
31+
"../../../../../../components/StatusMessages/IOAlertVisibleContext"
32+
),
33+
useIOAlertVisible: () => ({
34+
isAlertVisible: false,
35+
setAlertVisible: jest.fn()
36+
})
37+
})
38+
);
2939

3040
const mockNavigate = jest.fn();
3141
const mockReplace = jest.fn();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useCallback } from "react";
2+
import { VStack, IOButton } from "@pagopa/io-app-design-system";
3+
import I18n from "i18next";
4+
import { useIODispatch } from "../../../../store/hooks";
5+
import { OfflineAccessReasonEnum } from "../../../ingress/store/reducer";
6+
import { trackItwOfflineRicaricaAppIO } from "../../analytics";
7+
import { resetOfflineAccessReason } from "../../../ingress/store/actions";
8+
import { startupLoadSuccess } from "../../../../store/actions/startup";
9+
import { StartupStatusEnum } from "../../../../store/reducers/startup";
10+
import { useIOBottomSheetModal } from "../../../../utils/hooks/bottomSheet";
11+
import IOMarkdown from "../../../../components/IOMarkdown";
12+
import { useAppRestartAction } from "../../wallet/hooks/useAppRestartAction";
13+
14+
/**
15+
* Hook that creates and manages a bottom sheet modal to display detailed information
16+
* about the current offline state and provide app restart functionality.
17+
*
18+
* The modal includes:
19+
* - A title based on the specific offline reason
20+
* - Detailed explanation content rendered via IOMarkdown
21+
* - A button to attempt app restart when connectivity is restored
22+
*
23+
* When the restart button is pressed:
24+
* - If the device is connected, it will reset the offline state and restart the application
25+
* - If the device is still offline, it shows an error toast
26+
*
27+
* @param offlineAccessReason - The specific reason for the offline state, used to
28+
* determine the content and behavior of the modal
29+
* @returns An object with the bottom sheet modal controller (present, dismiss) and the modal component
30+
*/
31+
export const useOfflineAlertDetailModal = (
32+
offlineAccessReason: OfflineAccessReasonEnum
33+
) => {
34+
const dispatch = useIODispatch();
35+
const handleAppRestart = useAppRestartAction("bottom_sheet");
36+
37+
const navigateOnAuthPage = useCallback(() => {
38+
trackItwOfflineRicaricaAppIO("bottom_sheet");
39+
// Reset the offline access reason.
40+
// Since this state is `undefined` when the user is online,
41+
// the startup saga will proceed without blocking.
42+
dispatch(resetOfflineAccessReason());
43+
// Dispatch this action to mount the correct navigator.
44+
dispatch(startupLoadSuccess(StartupStatusEnum.NOT_AUTHENTICATED));
45+
}, [dispatch]);
46+
47+
const handlePressModalAction = useCallback(() => {
48+
if (offlineAccessReason === OfflineAccessReasonEnum.SESSION_EXPIRED) {
49+
navigateOnAuthPage();
50+
} else {
51+
handleAppRestart();
52+
}
53+
}, [handleAppRestart, navigateOnAuthPage, offlineAccessReason]);
54+
55+
return useIOBottomSheetModal({
56+
title: I18n.t(
57+
`features.itWallet.offline.${offlineAccessReason}.modal.title`
58+
),
59+
component: (
60+
<VStack space={24}>
61+
<IOMarkdown
62+
content={I18n.t(
63+
`features.itWallet.offline.${offlineAccessReason}.modal.content`
64+
)}
65+
/>
66+
<IOButton
67+
variant="solid"
68+
label={I18n.t(
69+
`features.itWallet.offline.${offlineAccessReason}.modal.footerAction`
70+
)}
71+
onPress={handlePressModalAction}
72+
/>
73+
</VStack>
74+
)
75+
});
76+
};

0 commit comments

Comments
 (0)