Skip to content

Commit 2a8b2fb

Browse files
authored
Merge pull request #8537 from Couchers-org/na/web/fix-banners
Fix push and cookie banner covering stuff
2 parents 5f1c8ac + 7460fd8 commit 2a8b2fb

9 files changed

Lines changed: 168 additions & 129 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { render, screen, waitFor } from "@testing-library/react";
2+
import useAuthStore from "features/auth/useAuthStore";
3+
import React from "react";
4+
import wrapper from "test/hookWrapper";
5+
6+
import { appGetLayout } from "./AppRoute";
7+
8+
jest.mock("features/auth/useAuthStore");
9+
jest.mock("components/Navigation", () => ({
10+
__esModule: true,
11+
default: () => null,
12+
}));
13+
jest.mock("components/Footer", () => ({
14+
__esModule: true,
15+
default: () => null,
16+
}));
17+
jest.mock("components/CookieBanner", () => ({
18+
__esModule: true,
19+
default: () => null,
20+
}));
21+
22+
jest.mock("features/notifications/PushNotificationBanner", () => ({
23+
PushNotificationBanner: () => (
24+
<div role="status" aria-label="Push notification banner">
25+
Push notification banner
26+
</div>
27+
),
28+
}));
29+
30+
const mockUseAuthStore = useAuthStore as jest.MockedFunction<
31+
typeof useAuthStore
32+
>;
33+
34+
describe("AppRoute", () => {
35+
beforeEach(() => {
36+
jest.clearAllMocks();
37+
});
38+
39+
it("renders the push notification banner when the user is authenticated", async () => {
40+
mockUseAuthStore.mockReturnValue({
41+
authState: {
42+
authenticated: true,
43+
error: null,
44+
jailed: false,
45+
loading: false,
46+
userId: 1,
47+
flowState: null,
48+
},
49+
authActions: {
50+
authError: jest.fn(),
51+
clearError: jest.fn(),
52+
firstLogin: jest.fn(),
53+
logout: jest.fn(),
54+
passwordLogin: jest.fn(),
55+
updateJailStatus: jest.fn(),
56+
updateSignupState: jest.fn(),
57+
},
58+
});
59+
60+
render(appGetLayout({ isPrivate: false })(<div>content</div>), { wrapper });
61+
62+
await waitFor(() => {
63+
expect(
64+
screen.getByLabelText("Push notification banner"),
65+
).toBeInTheDocument();
66+
});
67+
});
68+
69+
it("does not render the push notification banner when the user is not authenticated", () => {
70+
mockUseAuthStore.mockReturnValue({
71+
authState: {
72+
authenticated: false,
73+
error: null,
74+
jailed: false,
75+
loading: false,
76+
userId: null,
77+
flowState: null,
78+
},
79+
authActions: {
80+
authError: jest.fn(),
81+
clearError: jest.fn(),
82+
firstLogin: jest.fn(),
83+
logout: jest.fn(),
84+
passwordLogin: jest.fn(),
85+
updateJailStatus: jest.fn(),
86+
updateSignupState: jest.fn(),
87+
},
88+
});
89+
90+
render(appGetLayout({ isPrivate: false })(<div>content</div>), { wrapper });
91+
92+
expect(
93+
screen.queryByLabelText("Push notification banner"),
94+
).not.toBeInTheDocument();
95+
});
96+
});

app/web/components/AppRoute.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import CookieBanner from "components/CookieBanner";
55
import ErrorBoundary from "components/ErrorBoundary";
66
import Footer from "components/Footer";
77
import { useAuthContext } from "features/auth/AuthProvider";
8+
import { PushNotificationBanner } from "features/notifications/PushNotificationBanner";
89
import { useRouter } from "next/router";
9-
import { ReactNode, useEffect, useState } from "react";
10+
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
1011
import { jailRoute, loginRoute } from "routes";
1112
import { theme } from "theme";
1213
import { useIsNativeEmbed } from "utils/nativeLink";
@@ -61,12 +62,14 @@ const PageWrapper = styled(Box, {
6162
display: "flex",
6263
flexDirection: "column",
6364
flex: 1,
65+
paddingBottom: "var(--cookie-banner-height, 0px)",
6466
...(isNoOverflow && {
6567
overflow: "hidden",
6668
minHeight: 0,
6769
}),
6870
...(hasBottomNav && {
69-
paddingBottom: "calc(56px + env(safe-area-inset-bottom, 0px))",
71+
paddingBottom:
72+
"calc(56px + env(safe-area-inset-bottom, 0px) + var(--cookie-banner-height, 0px))",
7073
}),
7174
}),
7275
);
@@ -106,6 +109,24 @@ function AppRoute({
106109
const isJailed = authState.jailed;
107110
const isNativeEmbed = useIsNativeEmbed();
108111

112+
const headerRef = useRef<HTMLDivElement>(null);
113+
useLayoutEffect(() => {
114+
const updateNavHeight = () => {
115+
if (headerRef.current) {
116+
document.documentElement.style.setProperty(
117+
"--nav-height",
118+
`${headerRef.current.offsetHeight}px`,
119+
);
120+
}
121+
};
122+
updateNavHeight();
123+
const resizeObserver = new ResizeObserver(updateNavHeight);
124+
if (headerRef.current) {
125+
resizeObserver.observe(headerRef.current);
126+
}
127+
return () => resizeObserver.disconnect();
128+
}, [isAuthenticated, isNativeEmbed]);
129+
109130
//there must be the same loading state on auth'd pages on server and client
110131
//for hydration matching, so we will display a loader until mounted.
111132
const [isMounted, setIsMounted] = useState(false);
@@ -128,7 +149,11 @@ function AppRoute({
128149
) : (
129150
<>
130151
{variant === "no-overflow" ? globalStylesNoOverflow : globalStyles}
131-
<Navigation />
152+
<div ref={headerRef}>
153+
<Navigation />
154+
{!isNativeEmbed && isAuthenticated && <PushNotificationBanner />}
155+
</div>
156+
<CookieBanner />
132157
{/* Temporary container injected for marketing to test dynamic "announcements".
133158
* Find a better spot to componentise this code once plan is more finalised with this */}
134159
<div id="announcements"></div>
@@ -159,7 +184,6 @@ function AppRoute({
159184
</PageWrapper>
160185
</>
161186
)}
162-
{!isPrivate && <CookieBanner />}
163187
</ErrorBoundary>
164188
);
165189
}

app/web/components/CookieBanner.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ import { styled, Typography } from "@mui/material";
22
import IconButton from "components/IconButton";
33
import { CloseIcon } from "components/Icons";
44
import StyledLink from "components/StyledLink";
5-
import { useAuthContext } from "features/auth/AuthProvider";
65
import { Trans, useTranslation } from "i18n";
76
import { usePersistedState } from "platform/usePersistedState";
7+
import { useEffect, useRef } from "react";
88
import { tosRoute } from "routes";
99
import { useIsMounted } from "utils/hooks";
1010
import { useIsNativeEmbed } from "utils/nativeLink";
1111

1212
const StyledWrapper = styled("div")(({ theme }) => ({
1313
position: "fixed",
14+
bottom: 0,
15+
left: 0,
16+
right: 0,
1417
zIndex: theme.zIndex.snackbar,
15-
left: theme.spacing(0),
16-
right: theme.spacing(0),
1718
backgroundColor: "var(--mui-palette-background-paper)",
18-
bottom: 0,
1919
padding: theme.spacing(2, 4),
2020
boxShadow: theme.shadows[4],
2121

@@ -28,8 +28,7 @@ const StyledWrapper = styled("div")(({ theme }) => ({
2828

2929
const StyledCloseButton = styled(IconButton)(({ theme }) => ({
3030
position: "absolute",
31-
top: theme.spacing(4),
32-
transform: "translateY(-50%)",
31+
top: theme.spacing(1),
3332
right: theme.spacing(1),
3433
}));
3534

@@ -38,18 +37,33 @@ export default function CookieBanner() {
3837
// since we are using localStorage, make sure don't render unless mounted
3938
// or there will be hydration mismatches
4039
const isMounted = useIsMounted().current;
41-
const auth = useAuthContext();
4240
const [hasSeen, setHasSeen] = usePersistedState("hasSeenCookieBanner", false);
4341
const isNativeEmbed = useIsNativeEmbed();
42+
const bannerRef = useRef<HTMLDivElement>(null);
43+
44+
useEffect(() => {
45+
const el = bannerRef.current;
46+
if (!el) return;
47+
const updateHeight = () => {
48+
document.documentElement.style.setProperty(
49+
"--cookie-banner-height",
50+
`${el.offsetHeight}px`,
51+
);
52+
};
53+
updateHeight();
54+
const observer = new ResizeObserver(updateHeight);
55+
observer.observe(el);
56+
return () => {
57+
observer.disconnect();
58+
document.documentElement.style.removeProperty("--cookie-banner-height");
59+
};
60+
}, [hasSeen, isMounted]);
4461

45-
// Don't show cookie banner in native mobile app
46-
// Mobile apps use app store privacy mechanisms, not cookie banners
47-
// Only essential cookies are used (session/auth), which don't require consent
48-
if (auth.authState.authenticated || isNativeEmbed) return null;
62+
if (isNativeEmbed) return null;
4963

5064
//specifically not using our snackbar, which is designed for alerts
5165
return isMounted && !hasSeen ? (
52-
<StyledWrapper aria-live="polite">
66+
<StyledWrapper ref={bannerRef} aria-live="polite">
5367
<StyledCloseButton
5468
aria-label={t("close")}
5569
onClick={() => setHasSeen(true)}

app/web/components/Navigation/BottomNavigation.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
searchRoute,
2424
} from "routes";
2525

26-
const StyledPaper = styled(Paper)(({ theme }) => ({
26+
const StyledPaper = styled(Paper)(() => ({
2727
position: "fixed",
28-
bottom: 0,
28+
bottom: "var(--cookie-banner-height, 0px)",
2929
left: 0,
3030
right: 0,
3131
zIndex: 1100,

app/web/components/Navigation/Navigation.test.tsx

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@ jest.mock("features/donations/DonationBanner", () => ({
1515
),
1616
}));
1717

18-
jest.mock("features/notifications/PushNotificationBanner", () => ({
19-
PushNotificationBanner: () => (
20-
<div role="status" aria-label="Push notification banner">
21-
Push notification banner
22-
</div>
23-
),
24-
}));
25-
2618
const mockUseAuthStore = useAuthStore as jest.MockedFunction<
2719
typeof useAuthStore
2820
>;
@@ -61,37 +53,6 @@ describe("Navigation", () => {
6153
});
6254
});
6355

64-
it("renders the push notification banner when the user is authenticated", async () => {
65-
mockUseAuthStore.mockReturnValue({
66-
authState: {
67-
authenticated: true,
68-
error: null,
69-
jailed: false,
70-
loading: false,
71-
userId: 1,
72-
flowState: null,
73-
},
74-
authActions: {
75-
authError: jest.fn(),
76-
clearError: jest.fn(),
77-
firstLogin: jest.fn(),
78-
logout: jest.fn(),
79-
passwordLogin: jest.fn(),
80-
updateJailStatus: jest.fn(),
81-
updateSignupState: jest.fn(),
82-
},
83-
});
84-
85-
render(<Navigation />, { wrapper });
86-
87-
// Wait for component to mount and banners to appear
88-
await waitFor(() => {
89-
expect(
90-
screen.getByLabelText("Push notification banner"),
91-
).toBeInTheDocument();
92-
});
93-
});
94-
9556
it("does not render the donation banner when the user is not authenticated", () => {
9657
mockUseAuthStore.mockReturnValue({
9758
authState: {
@@ -117,32 +78,4 @@ describe("Navigation", () => {
11778

11879
expect(screen.queryByLabelText("Donation banner")).not.toBeInTheDocument();
11980
});
120-
121-
it("does not render the push notification banner when the user is not authenticated", () => {
122-
mockUseAuthStore.mockReturnValue({
123-
authState: {
124-
authenticated: false,
125-
error: null,
126-
jailed: false,
127-
loading: false,
128-
userId: null,
129-
flowState: null,
130-
},
131-
authActions: {
132-
authError: jest.fn(),
133-
clearError: jest.fn(),
134-
firstLogin: jest.fn(),
135-
logout: jest.fn(),
136-
passwordLogin: jest.fn(),
137-
updateJailStatus: jest.fn(),
138-
updateSignupState: jest.fn(),
139-
},
140-
});
141-
142-
render(<Navigation />, { wrapper });
143-
144-
expect(
145-
screen.queryByLabelText("Push notification banner"),
146-
).not.toBeInTheDocument();
147-
});
14881
});

0 commit comments

Comments
 (0)