From ef1a9ccb11f70898898b65498c150a45340f0c91 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Thu, 3 Aug 2023 14:50:04 -0600 Subject: [PATCH] 1490 passport banner (#1548) * feat(app): support banner component * feat(app): hook up warning component * chore(app): test support banner * fix(app): revert unecesarry type update --- app/.env-example.env | 2 +- .../components/SupportBanner.test.tsx | 51 +++++++++++++++ app/components/Header.tsx | 4 ++ app/components/SupportBanner.tsx | 62 +++++++++++++++++++ app/components/Warning.tsx | 7 ++- app/context/scorerContext.tsx | 2 +- app/context/userContext.tsx | 1 + 7 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 app/__tests__/components/SupportBanner.test.tsx create mode 100644 app/components/SupportBanner.tsx diff --git a/app/.env-example.env b/app/.env-example.env index 45c1245bed..d311ef8aff 100644 --- a/app/.env-example.env +++ b/app/.env-example.env @@ -53,7 +53,7 @@ NEXT_PUBLIC_INTERCOM_APP_ID=YOUR_INTERCOM_APP_ID NEXT_PUBLIC_ALLO_SCORER_ID=ALLO_SCORER_ID NEXT_PUBLIC_ALLO_SCORER_API_KEY=SCORER_API_KEY -NEXT_PUBLIC_SCORER_ENDPOINT=http://localhost:8002/ceramic-cache +NEXT_PUBLIC_SCORER_ENDPOINT=http://localhost:8002 NEXT_PUBLIC_FF_CHAIN_SYNC=on diff --git a/app/__tests__/components/SupportBanner.test.tsx b/app/__tests__/components/SupportBanner.test.tsx new file mode 100644 index 0000000000..2b5dab94aa --- /dev/null +++ b/app/__tests__/components/SupportBanner.test.tsx @@ -0,0 +1,51 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import axios from "axios"; +import { SupportBanner } from "../../components/SupportBanner"; +import { CeramicContextState } from "../../context/ceramicContext"; +import { UserContextState } from "../../context/userContext"; +import { + makeTestCeramicContext, + makeTestUserContext, + renderWithContext, +} from "../../__test-fixtures__/contextTestHelpers"; + +jest.mock("../../utils/onboard.ts"); + +const mockBannerResponse = [ + { + name: "Water Bottles", + content: "We are selling sweet water bottles", + link: "https://www.kleankanteen.com/", + banner_id: 1, + }, +]; + +const mockUserContext: UserContextState = makeTestUserContext(); +const mockCeramicContext: CeramicContextState = makeTestCeramicContext(); + +describe("SupportBanner", () => { + it("should render banner", async () => { + jest.spyOn(axios, "get").mockResolvedValueOnce({ data: mockBannerResponse }); + renderWithContext({ ...mockUserContext, dbAccessTokenStatus: "connected" }, mockCeramicContext, ); + + await screen.findByText(mockBannerResponse[0].content); + }); + it("should render banner with link", async () => { + jest.spyOn(axios, "get").mockResolvedValueOnce({ data: mockBannerResponse }); + renderWithContext({ ...mockUserContext, dbAccessTokenStatus: "connected" }, mockCeramicContext, ); + + const link = await screen.findByText("More information."); + expect(link).toHaveAttribute("href", mockBannerResponse[0].link); + }); + it("should dismiss banner", async () => { + const dismissCall = jest.spyOn(axios, "post"); + dismissCall.mockResolvedValueOnce({}); // Mock axios.post to resolve immediately + + jest.spyOn(axios, "get").mockResolvedValueOnce({ data: mockBannerResponse }); + + renderWithContext({ ...mockUserContext, dbAccessTokenStatus: "connected" }, mockCeramicContext, ); + const dismissBtn = await screen.findByText("Dismiss"); + fireEvent.click(dismissBtn); + await waitFor(() => expect(dismissCall).toHaveBeenCalled()); + }); +}); diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 9d2a5c73a7..2ab0ea1b3e 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -4,6 +4,7 @@ import { UserContext } from "../context/userContext"; import { PAGE_PADDING, CONTENT_MAX_WIDTH_INCLUDING_PADDING } from "./PageWidthGrid"; import Warning from "./Warning"; import MinimalHeader from "./MinimalHeader"; +import { SupportBanner } from "../components/SupportBanner"; type HeaderProps = { subheader?: React.ReactNode; @@ -20,6 +21,9 @@ const Header = ({ subheader }: HeaderProps): JSX.Element => {
{userWarning && setUserWarning()} />}
+
+ +
{subheader}
); diff --git a/app/components/SupportBanner.tsx b/app/components/SupportBanner.tsx new file mode 100644 index 0000000000..a8795133e7 --- /dev/null +++ b/app/components/SupportBanner.tsx @@ -0,0 +1,62 @@ +import axios from "axios"; +import { useContext, useEffect, useState } from "react"; +import { UserContext } from "../context/userContext"; +import { datadogRum } from "@datadog/browser-rum"; +import Warning from "./Warning"; + +type SupportBannerProps = { + content: string; + link: string; + banner_id: number; +}; + +export function SupportBanner() { + const [banners, setBanners] = useState([]); + const { dbAccessToken, dbAccessTokenStatus } = useContext(UserContext); + useEffect(() => { + (async function checkForBanners() { + if (dbAccessTokenStatus === "connected" && dbAccessToken) { + const banners: { + data: SupportBannerProps[]; + } = await axios.get(`${process.env.NEXT_PUBLIC_SCORER_ENDPOINT}/passport-admin/banners`, { + headers: { + Authorization: `Bearer ${dbAccessToken}`, + }, + }); + + setBanners(banners.data); + } + })(); + }, [dbAccessToken, dbAccessTokenStatus]); + + const dismissSupportBanner = async (banner_id: number) => { + try { + if (!dbAccessToken) throw new Error("No access token"); + await axios.post( + `${process.env.NEXT_PUBLIC_SCORER_ENDPOINT}/passport-admin/banners/${banner_id}/dismiss`, + {}, + { + headers: { + Authorization: `Bearer ${dbAccessToken}`, + }, + } + ); + setBanners(banners.filter((banner) => banner.banner_id !== banner_id)); + } catch (err) { + datadogRum.addError(err); + } + }; + + return ( + <> + {banners.map((banner) => ( + dismissSupportBanner(banner.banner_id)} + className="border-t border-accent-2 px-0" + /> + ))} + + ); +} diff --git a/app/components/Warning.tsx b/app/components/Warning.tsx index bc91e85c56..68fa79aa25 100644 --- a/app/components/Warning.tsx +++ b/app/components/Warning.tsx @@ -13,13 +13,18 @@ export default function Warning({ onDismiss: () => void; className?: string; }) { - const { content, dismissible, icon } = userWarning; + const { content, dismissible, icon, link } = userWarning; return (
{icon || } {content}{" "} + {link && ( + + More information. + + )} {dismissible && (