Skip to content

Commit ea008f1

Browse files
EKIRJASTO-626 Let user reveal and copy book passphrase (#67)
* EKIRJASTO-714 Use E-kirjasto OPDS parser version 2.1.0 * EKIRJASTO-714 Parse passphrases from book entry feed * EKIRJASTO-716 Expand Book type with LCP passphrase * EKIRJASTO-626 Refactor bookDetails component for clarity * EKIRJASTO-717 Create new BookPassphrase component * EKIRJASTO-715 Show instructions how to use book passphrase * EKIRJASTO-717 Display the password in easy-to-read text * EKIRJASTO-719 Copy book passhrase to clipboard * EKIRJASTO-718 Toggle book passphrase visibility * EKIRJASTO-720 Improve book passphrase component accessibility * EKIRJASTO-626 Update test snapshot * EKIRJASTO-626 Refactor bookDetails component tests for clarity * EKIRJASTO-626 Test book passphrase component
1 parent 0b3942b commit ea008f1

15 files changed

+766
-130
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@fortawesome/free-regular-svg-icons": "^6.7.2",
3535
"@fortawesome/free-solid-svg-icons": "^6.7.2",
3636
"@fortawesome/react-fontawesome": "^0.2.2",
37-
"@natlibfi/ekirjasto-opds-feed-parser": "2.0.1",
37+
"@natlibfi/ekirjasto-opds-feed-parser": "2.1.0",
3838
"@nypl/design-system-react-components": "^0.7.2",
3939
"@nypl/dgx-svg-icons": "^0.3.12",
4040
"@theme-ui/color": "^0.16.2",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { jsx } from "theme-ui";
7+
import BookPassphraseCopyButton from "components/BookPassphraseCopyButton";
8+
import BookPassphraseDisplayText from "components/BookPassphraseDisplayText";
9+
import React from "react";
10+
import Stack from "components/Stack";
11+
12+
// define props for the BookPassphraseCard component
13+
interface BookPassphraseCardProps {
14+
passphrase: string;
15+
}
16+
17+
// Component that displays the actual book passphrase string
18+
// and also a button for the user to easily copy the passphrase.
19+
// User can also select and copy the passphrase manually.
20+
const BookPassphraseCard: React.FC<BookPassphraseCardProps> = ({
21+
passphrase
22+
}) => {
23+
// define style for the Stack component
24+
const stackStyle: React.CSSProperties = {
25+
alignItems: "center"
26+
};
27+
28+
return (
29+
<Stack direction="row" sx={stackStyle} data-testid="book-passphrase-card">
30+
{/* first render the actual book passhprase as text */}
31+
<BookPassphraseDisplayText passphrase={passphrase} />
32+
33+
{/* then render the button to copy the book passhprase to clibpboard */}
34+
<BookPassphraseCopyButton stringToCopy={passphrase} />
35+
</Stack>
36+
);
37+
};
38+
39+
export default BookPassphraseCard;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7+
import { jsx } from "theme-ui";
8+
import {
9+
faCopy,
10+
faCheck,
11+
IconDefinition
12+
} from "@fortawesome/free-solid-svg-icons";
13+
import Button from "components/Button";
14+
import React, { useState, useEffect } from "react";
15+
16+
// define the text and icon for normal button state
17+
// as well as text and icon that indicate successful copying
18+
export const COPY_PASSPHRASE_TEXT: string = "Copy";
19+
export const COPY_PASSPHRASE_ICON: IconDefinition = faCopy;
20+
export const COPIED_PASSPHRASE_TEXT: string = "Copied!";
21+
export const COPIED_PASSPHRASE_ICON: IconDefinition = faCheck;
22+
23+
// define aria label for button
24+
export const COPY_PASSPHRASE_LABEL: string = "Copy book passphrase";
25+
26+
// define the timeout duration in milliseconds.
27+
// This should be enough time for the user to
28+
// notice the Copied! text before it disappears
29+
export const COPIED_PASSPHRASE_TIMEOUT: number = 2500;
30+
31+
// define props for the BookPassphraseCopyButton component
32+
interface BookPassphraseCopyButtonProps {
33+
stringToCopy: string;
34+
}
35+
36+
// Component that renders a button to copy a string to the user's clipboard
37+
// The string that will be copied to clipboard is given as parameter
38+
const BookPassphraseCopyButton: React.FC<BookPassphraseCopyButtonProps> = ({
39+
stringToCopy
40+
}) => {
41+
// define a button state "isCopied"
42+
// so we can keep tabs if button was clicked
43+
const [isCopied, setIsCopied] = useState<boolean>(false);
44+
45+
// function to copy text to clipboard
46+
// if copying was successful, set new button state
47+
const copyToClipboard = async () => {
48+
try {
49+
await navigator.clipboard.writeText(stringToCopy);
50+
setIsCopied(true);
51+
} catch (error) {
52+
console.error("Error copying to clipboard:", error);
53+
}
54+
};
55+
56+
// reset button after 2,5 seconds
57+
// return the normal state for button
58+
useEffect(() => {
59+
if (isCopied) {
60+
const timer = setTimeout(() => {
61+
setIsCopied(false);
62+
}, COPIED_PASSPHRASE_TIMEOUT);
63+
return () => clearTimeout(timer);
64+
}
65+
}, [isCopied]);
66+
67+
// check if the clipboard API is available in the user's browser
68+
const isClipboardAvailable = Boolean(navigator.clipboard);
69+
70+
// get the text and icon based on copied state
71+
const { buttonText, buttonIcon } = isCopied
72+
? { buttonText: COPIED_PASSPHRASE_TEXT, buttonIcon: COPIED_PASSPHRASE_ICON }
73+
: { buttonText: COPY_PASSPHRASE_TEXT, buttonIcon: COPY_PASSPHRASE_ICON };
74+
75+
// define the style for the button
76+
// gap is the space between text and icon inside the button
77+
const buttonStyle: React.CSSProperties = {
78+
width: "110px",
79+
height: "30px",
80+
display: "flex",
81+
alignItems: "center",
82+
justifyContent: "center",
83+
gap: "8px"
84+
};
85+
86+
return (
87+
<>
88+
{/* copy button is only visible if clipboard is available */}
89+
{isClipboardAvailable && (
90+
<Button
91+
onClick={copyToClipboard}
92+
sx={buttonStyle}
93+
aria-label={COPY_PASSPHRASE_LABEL}
94+
data-testid="book-passphrase-copy-button"
95+
>
96+
{buttonText}
97+
<FontAwesomeIcon icon={buttonIcon} />
98+
</Button>
99+
)}
100+
</>
101+
);
102+
};
103+
104+
export default BookPassphraseCopyButton;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { jsx } from "theme-ui";
7+
import { Text } from "components/Text";
8+
import React from "react";
9+
10+
// define aria label for button
11+
export const PASSPHRASE_LABEL: string = "Book passphrase";
12+
13+
// define props for the BookPassphraseDisplayText component
14+
interface BookPassphraseDisplayTextProps {
15+
passphrase: string;
16+
}
17+
18+
// PassphraseDisplayText component is for displaying the actual passphrase
19+
const BookPassphraseDisplayText: React.FC<BookPassphraseDisplayTextProps> = ({
20+
passphrase
21+
}) => {
22+
// define style for the Stack component
23+
// for example use monospace font
24+
const textStyle: React.CSSProperties = {
25+
fontFamily: "monospace",
26+
fontSize: "-1",
27+
wordBreak: "break-all",
28+
backgroundColor: "ui.gray.lightWarm",
29+
padding: "5px",
30+
borderRadius: "1"
31+
};
32+
33+
return (
34+
<Text
35+
sx={textStyle}
36+
aria-label={PASSPHRASE_LABEL}
37+
data-testid="book-passphrase-display-text"
38+
>
39+
{passphrase}
40+
</Text>
41+
);
42+
};
43+
44+
export default BookPassphraseDisplayText;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { jsx } from "theme-ui";
7+
import { Text } from "components/Text";
8+
import React from "react";
9+
10+
// define the instructions text string
11+
export const INSTRUCTIONS_TEXT: string =
12+
"If you download the book, please copy the password below for your EPUB reader.";
13+
14+
// Component that instructs the user
15+
// so that user knows how to use the book passphrase
16+
const BookPassphraseInstructionsText: React.FC = () => {
17+
// define style for the intruction text
18+
const textStyle: React.CSSProperties = {
19+
fontSize: "-1"
20+
};
21+
22+
return (
23+
<Text
24+
sx={textStyle}
25+
aria-live="polite"
26+
data-testid="book-passphrase-instructions-text"
27+
>
28+
{INSTRUCTIONS_TEXT}
29+
</Text>
30+
);
31+
};
32+
33+
export default BookPassphraseInstructionsText;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { jsx } from "theme-ui";
7+
import Button from "components/Button";
8+
import React from "react";
9+
10+
// define the alternating texts for the toggle button
11+
export const HIDE_PASSPHRASE_TEXT: string = "Hide passphrase";
12+
export const SHOW_PASSPHRASE_TEXT: string = "Show passphrase";
13+
14+
// define props for the BookPassphraseToggleButton component
15+
// isVisible indicates if the passphrase card is visible
16+
// onToggle is function that toggles the visibility of the passphrase card
17+
interface BookPassphraseToggleButtonProps {
18+
isVisible: boolean;
19+
onToggle: () => void;
20+
}
21+
22+
// Component that renders a button that toggles the visibility of the passphrase card
23+
const BookPassphraseToggleButton: React.FC<BookPassphraseToggleButtonProps> = ({
24+
isVisible,
25+
onToggle
26+
}) => {
27+
// change text based on if passhphrase is currently visible or not
28+
const buttonText = isVisible ? HIDE_PASSPHRASE_TEXT : SHOW_PASSPHRASE_TEXT;
29+
30+
// define the style for the toggle button
31+
const buttonStyle: React.CSSProperties = {
32+
display: "flex",
33+
alignItems: "center",
34+
justifyContent: "center"
35+
};
36+
37+
return (
38+
<Button
39+
onClick={onToggle}
40+
sx={buttonStyle}
41+
aria-pressed={isVisible}
42+
aria-label={buttonText}
43+
data-testid="book-passphrase-toggle-button"
44+
>
45+
{buttonText}
46+
</Button>
47+
);
48+
};
49+
50+
export default BookPassphraseToggleButton;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-nocheck
5+
6+
import { AnyBook } from "interfaces";
7+
import { jsx } from "theme-ui";
8+
import * as React from "react";
9+
import BookPassphraseCard from "components/BookPassphraseCard";
10+
import BookPassphraseInstructionsText from "components/BookPassphraseInstructionsText";
11+
import BookPassphraseToggleButton from "components/BookPassphraseToggleButton";
12+
import Stack from "components/Stack";
13+
import useUser from "components/context/UserContext";
14+
15+
// define props for the BookPassphrase component
16+
interface BookPassphraseProps {
17+
book: AnyBook;
18+
}
19+
20+
// Component to help user handle the book passhrase.
21+
// Book passhrase is a LCP password string.
22+
// Users need their personal passphrase
23+
// when they download a book file and
24+
// open it in a separate reader software
25+
const BookPassphrase: React.FC<BookPassphraseProps> = ({ book }) => {
26+
// get the user's authentication status
27+
const { isAuthenticated } = useUser();
28+
29+
// check if book is an eBook format (ePub or PDF)
30+
const isEbook = book.format === "ePub" || book.format === "PDF";
31+
32+
// check if book has a passphrase
33+
const hasPassphrase = book.passphrase !== undefined;
34+
35+
// determine if the passphrase should be shown
36+
const shouldShowPassphrase = isAuthenticated && hasPassphrase && isEbook;
37+
38+
// define a button state "isVisible"
39+
// so we can keep tabs if passphrase card is visible
40+
const [isVisible, setVisible] = React.useState(false);
41+
42+
// function that toggles the visibility of the passphrase card
43+
const toggleVisibility = () => {
44+
setVisible(previousState => !previousState);
45+
};
46+
47+
// define style for the Stack component
48+
const stackStyle: React.CSSProperties = {
49+
alignItems: "flex-start",
50+
marginBottom: "16px"
51+
};
52+
53+
// component is only rendered if the
54+
// - user is logged-in
55+
// - book passphrase exists
56+
// - book is ePub or PDF
57+
return (
58+
<>
59+
{shouldShowPassphrase && (
60+
<Stack
61+
direction="column"
62+
sx={stackStyle}
63+
data-testid="book-passphrase-component"
64+
>
65+
{/* first render instructions how to use book passhprase */}
66+
<BookPassphraseInstructionsText />
67+
68+
{/* render the toggle button */}
69+
<BookPassphraseToggleButton
70+
isVisible={isVisible}
71+
onToggle={toggleVisibility}
72+
/>
73+
74+
{/* then render the passphrase card based on visibility state */}
75+
{isVisible && <BookPassphraseCard passphrase={book.passphrase} />}
76+
</Stack>
77+
)}
78+
</>
79+
);
80+
};
81+
82+
export default BookPassphrase;

0 commit comments

Comments
 (0)