Skip to content

Commit

Permalink
feat: added check login required for export/download #283 (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
MillenniumFalconMechanic authored Feb 28, 2025
1 parent 95effbf commit badb6ab
Show file tree
Hide file tree
Showing 26 changed files with 706 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Tooltip } from "@mui/material";
import React, { ElementType, ReactNode } from "react";
import { useDownloadStatus } from "../../../../../../hooks/useDownloadStatus";
import { useFileManifestState } from "../../../../../../hooks/useFileManifestState";
import { useLoginGuard } from "../../../../../../providers/loginGuard/hook";
import { ButtonPrimary } from "../../../../../common/Button/components/ButtonPrimary/buttonPrimary";

export interface ExportButtonProps {
Expand All @@ -19,14 +20,20 @@ export const ExportButton = ({
const {
fileManifestState: { isLoading },
} = useFileManifestState();

// Prompt user for login before export, if required.
const { requireLogin } = useLoginGuard();

return (
<Tooltip arrow title={isLoading ? null : downloadStatus.message}>
<span>
<Button
disabled={
isLoading || downloadStatus.disabled || downloadStatus.isLoading
}
onClick={onClick}
onClick={() => {
requireLogin(onClick);
}}
>
<span>{children}</span>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, { useRef } from "react";
import { Filters } from "../../../../../../../../common/entities";
import { useDownloadStatus } from "../../../../../../../../hooks/useDownloadStatus";
import { useFileManifestDownload } from "../../../../../../../../hooks/useFileManifest/useFileManifestDownload";
import { useLoginGuard } from "../../../../../../../../providers/loginGuard/hook";
import { ButtonGroup } from "../../../../../../../common/ButtonGroup/buttonGroup";
import { ButtonGroupButton } from "../../../../../../../common/ButtonGroup/components/ButtonGroupButton/buttonGroupButton";
import {
Expand Down Expand Up @@ -46,6 +47,9 @@ export const FileManifestDownload = ({
const isInProgress = (isIdle || isLoading) && !disabled;
const isReady = Boolean(manifestURL) || disabled;

// Prompt user for login before download and copy, if required.
const { requireLogin } = useLoginGuard();

// Copies file manifest.
const copyManifestURL = (url?: string): void => {
if (!url) return;
Expand Down Expand Up @@ -89,15 +93,19 @@ export const FileManifestDownload = ({
action="Download file manifest"
disabled={disabled}
label={<DownloadIconSmall />}
onClick={downloadManifestURL}
onClick={() =>
requireLogin(downloadManifestURL)
}
/>,
<ButtonGroupButton
key="copy"
action="Copy file manifest"
disabled={disabled}
label={<ContentCopyIconSmall />}
onClick={(): void =>
copyManifestURL(manifestURL)
onClick={() =>
requireLogin((): void =>
copyManifestURL(manifestURL)
)
}
/>,
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box } from "@mui/material";
import React, { Fragment, useEffect, useRef, useState } from "react";
import { useFileLocation } from "../../../../hooks/useFileLocation";
import { useLoginGuard } from "../../../../providers/loginGuard/hook";
import { DownloadIcon } from "../../../common/CustomIcon/components/DownloadIcon/downloadIcon";
import { LoadingIcon } from "../../../common/CustomIcon/components/LoadingIcon/loadingIcon";
import { IconButton } from "../../../common/IconButton/iconButton";
Expand Down Expand Up @@ -29,6 +30,9 @@ export const AzulFileDownload = ({
const downloadRef = useRef<HTMLAnchorElement>(null);
const [isRequestPending, setIsRequestPending] = useState(false);

// Prompt user for login before download, if required.
const { requireLogin } = useLoginGuard();

// Initiates file download when file location request is successful.
useEffect(() => {
if (!fileUrl) return;
Expand All @@ -39,6 +43,13 @@ export const AzulFileDownload = ({
setIsRequestPending(false);
}, [fileUrl]);

// Initiates file download when download button is clicked.
const handleDownloadClick = (): void => {
setIsRequestPending(true);
trackFileDownloaded(entityName, relatedEntityId, relatedEntityName);
run();
};

return (
<Fragment>
{isRequestPending ? (
Expand All @@ -54,11 +65,7 @@ export const AzulFileDownload = ({
data-testid={AZUL_FILE_REQUEST_DOWNLOAD_TEST_ID}
disabled={!url}
Icon={isLoading ? LoadingIcon : DownloadIcon}
onClick={(): void => {
setIsRequestPending(true);
trackFileDownloaded(entityName, relatedEntityId, relatedEntityName);
run();
}}
onClick={() => requireLogin(handleDownloadClick)}
size="medium"
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Login/components/Button/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ButtonProps } from "@mui/material/Button/Button";
import { ButtonProps } from "@mui/material";
import { BaseComponentProps } from "../../../../theme/common/entities";

export type Props = BaseComponentProps & ButtonProps;
22 changes: 22 additions & 0 deletions src/components/Login/components/Buttons/buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { Button } from "../Button/button";
import { Props } from "./types";

export const Buttons = <P,>({
className,
handleLogin,
providers = [],
...props /* Mui ButtonProps */
}: Props<P>): JSX.Element[] => {
return providers?.map((provider) => (
<Button
key={provider.id}
className={className}
endIcon={"icon" in provider && provider.icon}
onClick={() => handleLogin(provider.id)}
{...props}
>
{provider.name}
</Button>
));
};
9 changes: 9 additions & 0 deletions src/components/Login/components/Buttons/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ButtonProps } from "@mui/material";
import { ClientSafeProvider } from "next-auth/react";
import { OAuthProvider } from "../../../../config/entities";
import { BaseComponentProps } from "../../../../theme/common/entities";

export interface Props<P> extends BaseComponentProps, ButtonProps {
handleLogin: (providerId: string) => void;
providers?: ClientSafeProvider[] | OAuthProvider<P>[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import styled from "@emotion/styled";
import { Grid2 } from "@mui/material";

export const StyledGrid2 = styled(Grid2)`
align-items: center;
align-self: flex-start;
display: flex;
gap: 12px;
.MuiTypography-text-body-400 {
.MuiLink-root {
color: inherit;
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Checkbox, Typography } from "@mui/material";
import React from "react";
import { TEXT_BODY_400 } from "../../../../../../theme/common/typography";
import { CheckedIcon } from "../../../../../common/CustomIcon/components/CheckedIcon/checkedIcon";
import { UncheckedErrorIcon } from "../../../../../common/CustomIcon/components/UncheckedErrorIcon/uncheckedErrorIcon";
import { UncheckedIcon } from "../../../../../common/CustomIcon/components/UncheckedIcon/uncheckedIcon";
import { BaseComponentProps } from "../../../../../types";
import { StyledGrid2 } from "./consent.styles";
import { ConsentProps } from "./types";

export const Consent = ({
children,
className,
handleConsent,
isDisabled,
isError,
...props /* Mui Grid2Props */
}: BaseComponentProps & ConsentProps): JSX.Element | null => {
if (isDisabled) return null;
return (
<StyledGrid2 className={className} {...props}>
<Checkbox
checkedIcon={<CheckedIcon />}
icon={isError ? <UncheckedErrorIcon /> : <UncheckedIcon />}
onChange={handleConsent}
/>
<Typography variant={TEXT_BODY_400}>{children}</Typography>
</StyledGrid2>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Grid2Props } from "@mui/material";
import { ReactNode } from "react";
import { UseUserConsent } from "../../../../hooks/useUserConsent/types";

export interface ConsentProps
extends Grid2Props,
Pick<UseUserConsent, "handleConsent">,
Pick<UseUserConsent["state"], "isDisabled" | "isError"> {
children: ReactNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Typography, TypographyProps } from "@mui/material";
import React from "react";
import { COLOR } from "../../../../../../styles/common/mui/typography";
import { TEXT_BODY_SMALL_400 } from "../../../../../../theme/common/typography";
import { BaseComponentProps } from "../../../../../types";

export const Warning = ({
children,
className,
...props /* Mui TypographyOwnProps */
}: BaseComponentProps & TypographyProps): JSX.Element | null => {
if (!children) return null;
return (
<Typography
className={className}
color={COLOR.INK_LIGHT}
mt={6}
variant={TEXT_BODY_SMALL_400}
{...props}
>
{children}
</Typography>
);
};
11 changes: 11 additions & 0 deletions src/components/Login/hooks/useUserConsent/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ChangeEvent } from "react";

export interface UseUserConsent {
handleConsent: (e: ChangeEvent<HTMLInputElement>) => void;
handleError: (error: boolean) => void;
state: {
isDisabled: boolean;
isError: boolean;
isValid: boolean;
};
}
32 changes: 32 additions & 0 deletions src/components/Login/hooks/useUserConsent/useUserConsent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeEvent, useCallback, useState } from "react";
import { useAuthenticationConfig } from "../../../../hooks/authentication/config/useAuthenticationConfig";
import { UseUserConsent } from "./types";

export const useUserConsent = (): UseUserConsent => {
const authConfig = useAuthenticationConfig();
const [isDisabled] = useState<boolean>(Boolean(!authConfig?.termsOfService));
const [isError, setIsError] = useState<boolean>(false);
const [isValid, setIsValid] = useState<boolean>(false);

const handleError = useCallback((error: boolean) => {
setIsError(error);
}, []);

const handleConsent = useCallback(
(changeEvent: ChangeEvent<HTMLInputElement>): void => {
handleError(false);
setIsValid(changeEvent.target.checked);
},
[handleError]
);

return {
handleConsent,
handleError,
state: {
isDisabled,
isError,
isValid,
},
};
};
8 changes: 8 additions & 0 deletions src/components/Login/hooks/useUserLogin/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ProviderId } from "../../../../providers/authentication/common/types";
import { UseUserConsent } from "../useUserConsent/types";

export interface UseUserLogin
extends Omit<UseUserConsent, "handleError" | "state"> {
consentState: Pick<UseUserConsent["state"], "isDisabled" | "isError">;
handleLogin: (providerId: ProviderId) => void;
}
29 changes: 29 additions & 0 deletions src/components/Login/hooks/useUserLogin/useUserLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback } from "react";
import { useAuth } from "../../../../providers/authentication/auth/hook";
import { ProviderId } from "../../../../providers/authentication/common/types";
import { useUserConsent } from "../useUserConsent/useUserConsent";
import { UseUserLogin } from "./types";

export const useUserLogin = (): UseUserLogin => {
const { service: { requestLogin } = {} } = useAuth();
const { handleConsent, handleError, state: consentState } = useUserConsent();
const { isDisabled, isError, isValid } = consentState; // Consent state: { isValid } is an indicator of whether the user has accepted the login terms.

const handleLogin = useCallback(
(providerId: ProviderId): void => {
if (!isDisabled && !isValid) {
// If the user has not accepted terms, set error state to true.
handleError(true);
return;
}
requestLogin?.(providerId);
},
[handleError, isDisabled, isValid, requestLogin]
);

return {
consentState: { isDisabled, isError },
handleConsent,
handleLogin,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
import React from "react";

export const CloseIcon = ({
fontSize = "xsmall",
viewBox = "0 0 18 18",
...props /* Spread props to allow for Mui SvgIconProps specific prop overrides e.g. "htmlColor". */
}: SvgIconProps): JSX.Element => {
return (
<SvgIcon fontSize={fontSize} viewBox={viewBox} {...props}>
<path
d="M8.99994 10.1061L5.38104 13.725C5.23104 13.875 5.04984 13.947 4.83744 13.941C4.62504 13.9344 4.44384 13.8561 4.29384 13.7061C4.14384 13.5561 4.06884 13.3719 4.06884 13.1535C4.06884 12.9345 4.14384 12.75 4.29384 12.6L7.89384 9.00005L4.27494 5.38115C4.12494 5.23115 4.05294 5.04695 4.05894 4.82855C4.06554 4.60955 4.14384 4.42505 4.29384 4.27505C4.44384 4.12505 4.62804 4.05005 4.84644 4.05005C5.06544 4.05005 5.24994 4.12505 5.39994 4.27505L8.99994 7.89395L12.6188 4.27505C12.7688 4.12505 12.953 4.05005 13.1714 4.05005C13.3904 4.05005 13.5749 4.12505 13.7249 4.27505C13.8749 4.42505 13.9499 4.60955 13.9499 4.82855C13.9499 5.04695 13.8749 5.23115 13.7249 5.38115L10.106 9.00005L13.7249 12.6189C13.8749 12.7689 13.9499 12.9501 13.9499 13.1625C13.9499 13.3749 13.8749 13.5561 13.7249 13.7061C13.5749 13.8561 13.3904 13.9311 13.1714 13.9311C12.953 13.9311 12.7688 13.8561 12.6188 13.7061L8.99994 10.1061Z"
fill="currentColor"
/>
</SvgIcon>
);
};
33 changes: 33 additions & 0 deletions src/components/common/LoginDialog/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
DialogContentTextProps,
DialogProps,
DialogTitleProps,
IconButtonProps,
IconProps,
} from "@mui/material";
import { FONT_SIZE } from "../../../styles/common/mui/icon";
import { COLOR, VARIANT } from "../../../styles/common/mui/typography";

export const DIALOG_CONTENT_TEXT_PROPS: DialogContentTextProps = {
color: COLOR.INK_LIGHT,
component: "div",
variant: VARIANT.TEXT_BODY_400,
};

export const DIALOG_PROPS: Partial<DialogProps> = {
PaperProps: { elevation: 0 },
};

export const DIALOG_TITLE_PROPS: DialogTitleProps = {
variant: VARIANT.TEXT_HEADING_SMALL,
};

export const ICON_BUTTON_PROPS: IconButtonProps = {
color: "inkLight",
edge: "end",
size: "xsmall",
};

export const ICON_PROPS: Pick<IconProps, "fontSize"> = {
fontSize: FONT_SIZE.SMALL,
};
Loading

0 comments on commit badb6ab

Please sign in to comment.