Skip to content

Commit

Permalink
Merge pull request #8 from rezkiy37/feature/22998-pdf-password-handling
Browse files Browse the repository at this point in the history
Feature/22998 pdf password handling
  • Loading branch information
mountiny authored Dec 11, 2023
2 parents e51dff9 + 34f3717 commit 55f09e3
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 36 deletions.
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 = {
/** 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

0 comments on commit 55f09e3

Please sign in to comment.