Skip to content

Commit

Permalink
1490 passport banner (#1548)
Browse files Browse the repository at this point in the history
* feat(app): support banner component

* feat(app): hook up warning component

* chore(app): test support banner

* fix(app): revert unecesarry type update
  • Loading branch information
schultztimothy authored Aug 3, 2023
1 parent 2aad042 commit ef1a9cc
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 3 deletions.
2 changes: 1 addition & 1 deletion app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 51 additions & 0 deletions app/__tests__/components/SupportBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -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, <SupportBanner />);

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, <SupportBanner />);

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, <SupportBanner />);
const dismissBtn = await screen.findByText("Dismiss");
fireEvent.click(dismissBtn);
await waitFor(() => expect(dismissCall).toHaveBeenCalled());
});
});
4 changes: 4 additions & 0 deletions app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,9 @@ const Header = ({ subheader }: HeaderProps): JSX.Element => {
<div className={`w-full bg-red-100 ${PAGE_PADDING}`}>
{userWarning && <Warning userWarning={userWarning} onDismiss={() => setUserWarning()} />}
</div>
<div className="w-full bg-red-100">
<SupportBanner />
</div>
<div className={`mx-auto w-full ${PAGE_PADDING} ${CONTENT_MAX_WIDTH_INCLUDING_PADDING}`}>{subheader}</div>
</div>
);
Expand Down
62 changes: 62 additions & 0 deletions app/components/SupportBanner.tsx
Original file line number Diff line number Diff line change
@@ -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<SupportBannerProps[]>([]);
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) => (
<Warning
key={banner.banner_id}
userWarning={{ content: banner.content, link: banner.link, dismissible: true }}
onDismiss={() => dismissSupportBanner(banner.banner_id)}
className="border-t border-accent-2 px-0"
/>
))}
</>
);
}
7 changes: 6 additions & 1 deletion app/components/Warning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ export default function Warning({
onDismiss: () => void;
className?: string;
}) {
const { content, dismissible, icon } = userWarning;
const { content, dismissible, icon, link } = userWarning;
return (
<div
className={`mx-auto flex w-full items-center justify-center py-2 text-purple-darkpurple ${CONTENT_MAX_WIDTH} ${className}`}
>
{icon || <ExclamationCircleIcon height={24} color={"#D44D6E"} className="mr-4" />}
{content}{" "}
{link && (
<a href={link} target="_blank" rel="noreferrer" className="ml-2 underline">
More information.
</a>
)}
{dismissible && (
<button onClick={onDismiss} className="ml-2 underline">
Dismiss
Expand Down
2 changes: 1 addition & 1 deletion app/context/scorerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { createContext, useState } from "react";
// --- Axios
import axios, { AxiosError } from "axios";

const scorerApiGetScore = process.env.NEXT_PUBLIC_SCORER_ENDPOINT + "/score";
const scorerApiGetScore = process.env.NEXT_PUBLIC_SCORER_ENDPOINT + "/ceramic-cache/score";

const isLiveAlloScoreEnabled = process.env.NEXT_PUBLIC_FF_LIVE_ALLO_SCORE === "on";

Expand Down
1 change: 1 addition & 0 deletions app/context/userContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface UserWarning {
icon?: React.ReactNode;
name?: UserWarningName;
dismissible?: boolean;
link?: string;
}

export interface UserContextState {
Expand Down

0 comments on commit ef1a9cc

Please sign in to comment.