Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/22998 pdf password handling #8

Merged
merged 13 commits into from
Dec 11, 2023
25 changes: 15 additions & 10 deletions example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-fast-pdf": "git+https://github.com/rezkiy37/react-fast-pdf.git#feature/22998-pdf-preview-component"
"react-fast-pdf": "^1.0.2"
},
"devDependencies": {
"@babel/cli": "^7.22.9",
Expand Down
Binary file added example/public/example_protected.pdf
Binary file not shown.
54 changes: 44 additions & 10 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
import React, {CSSProperties} from 'react';
// TODO: Change the package source in package.json
// TODO: Use built source code, not initial
import React, {CSSProperties, useState} from 'react';
import ReactFastPDF, {PDFPreviewer} from 'react-fast-pdf';
import './index.css';

const pdfPreviewerContainerStyles: CSSProperties = {
const pdfPreviewerContainerStyle: CSSProperties = {
borderRadius: 4,
borderWidth: 2,
borderColor: '#184E3D',
borderStyle: 'solid',
};

function App() {
const [file, setFile] = useState<string | null>(null);

return (
<main className="container">
<h1 className="title">Hello, I am {ReactFastPDF.PackageName}!</h1>

<PDFPreviewer
file="/example.pdf"
pageMaxWidth={1000}
isSmallScreen={false}
containerStyle={pdfPreviewerContainerStyles}
/>
{file ? (
<>
<button
className="button button_back"
type="button"
onClick={() => setFile(null)}
>
Back
</button>

<PDFPreviewer
file={file}
pageMaxWidth={1000}
isSmallScreen={false}
containerStyle={pdfPreviewerContainerStyle}
/>
</>
) : (
<>
<h3>Please choose a file for previewing:</h3>

<div className="buttons_container">
<button
className="button"
type="button"
onClick={() => setFile('example.pdf')}
>
example.pdf
</button>

<button
className="button"
type="button"
onClick={() => setFile('example_protected.pdf')}
>
example_protected.pdf
</button>
</div>
</>
)}
</main>
);
}
Expand Down
33 changes: 29 additions & 4 deletions example/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
url('../assets/fonts/ExpensifyNeue-Bold.woff') format('woff');
}

* {
margin: 0;
color: #e7ece9;
font-family: ExpensifyMono-Neue;
}

body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}

Expand All @@ -29,11 +34,31 @@ body {
}

.title {
margin: 0 0 16px;
font-family: ExpensifyMono-Neue;
color: #e7ece9;
margin-bottom: 16px;
}

.react-pdf__Page {
background-color: transparent !important;
}

.buttons_container {
margin-top: 16px;
display: flex;
}

.button {
margin: 0 8px;
padding: 8px 16px;
font-size: 18px;
border: 0;
border-radius: 16px;
background-color: #03d47c;
cursor: 'pointer';
}

.button_back {
position: absolute;
z-index: 1;
left: 16px;
top: 16px;
}
162 changes: 162 additions & 0 deletions src/PDFPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, {useState, useRef, useMemo, type ChangeEvent, type FormEventHandler} from 'react';
import PropTypes from 'prop-types';

import {pdfPasswordFormStyles as styles} from './styles';
import {isSafari} from './helpers';

type Props = {
isPasswordInvalid?: boolean;
isFocused: boolean;
onSubmit?: (password: string) => void;
onPasswordChange?: (password: string) => void;
onPasswordFieldFocus?: (isFocused: boolean) => void;
};

const propTypes = {
rezkiy37 marked this conversation as resolved.
Show resolved Hide resolved
/** If the submitted password is invalid (show an error message) */
isPasswordInvalid: PropTypes.bool,

/** Should focus to the password input */
isFocused: PropTypes.bool.isRequired,

/** Notify parent that the password form has been submitted */
onSubmit: PropTypes.func,

/** Notify parent that the password has been updated/edited */
onPasswordChange: PropTypes.func,

/** Notify parent that a text field has been focused or blurred */
onPasswordFieldFocus: PropTypes.func,
};

const defaultProps = {
isPasswordInvalid: false,
onSubmit: () => {},
onPasswordChange: () => {},
onPasswordFieldFocus: () => {},
};

function PDFPasswordForm({isPasswordInvalid, isFocused, onSubmit, onPasswordChange, onPasswordFieldFocus}: Props) {
const [password, setPassword] = useState('');
const [validationErrorText, setValidationErrorText] = useState('');
const [shouldShowForm, setShouldShowForm] = useState(false);

const textInputRef = useRef<HTMLInputElement>(null);

const errorText = useMemo(() => {
if (isPasswordInvalid) {
return 'Incorrect password. Please try again.';
}

if (validationErrorText) {
return validationErrorText;
}

return '';
}, [isPasswordInvalid, validationErrorText]);

const updatePassword = (event: ChangeEvent<HTMLInputElement>) => {
const newPassword = event.target.value;

setPassword(newPassword);
onPasswordChange?.(newPassword);
setValidationErrorText('');
};

const validate = () => {
if (!isPasswordInvalid && password) {
return true;
}

if (!password) {
setValidationErrorText('Password required. Pleaser enter.');
}

return false;
};

const submitPassword: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();

if (!validate()) {
return;
}

onSubmit?.(password);
};

const validateAndNotifyPasswordBlur = () => {
validate();

onPasswordFieldFocus?.(false);
};

if (!shouldShowForm) {
return (
<div style={styles.container}>
<p style={styles.infoMessage}>
This PDF is password protected.
<br />
Please&nbsp;
<span
style={styles.infoMessageButton}
aria-hidden="true"
onClick={() => setShouldShowForm(true)}
>
enter the password
</span>
&nbsp;to view it.
</p>
</div>
);
}

return (
<div style={styles.container}>
<p style={styles.infoMessage}>View PDF</p>

<form
style={styles.form}
onSubmit={submitPassword}
>
<label
style={styles.inputLabel}
htmlFor="password"
>
Password:
<input
style={styles.input}
ref={textInputRef}
id="password"
value={password}
/**
* This is a workaround to bypass Safari's autofill odd behaviour.
* This tricks the browser not to fill the username somewhere else and still fill the password correctly.
*/
autoComplete={isSafari() ? 'username' : 'off'}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={isFocused}
type="password"
onFocus={() => onPasswordFieldFocus?.(true)}
onBlur={validateAndNotifyPasswordBlur}
onChange={updatePassword}
/>
</label>

{!!errorText && <p style={styles.errorMessage}>{errorText}</p>}

<input
style={styles.confirmButton}
type="submit"
value="Confirm"
/>
</form>
</div>
);
}

PDFPasswordForm.propTypes = propTypes;
PDFPasswordForm.defaultProps = defaultProps;
PDFPasswordForm.displayName = 'PDFPasswordForm';

export default PDFPasswordForm;
Loading
Loading