Skip to content

Commit

Permalink
Merge pull request #9141 from frenkield/frenkield-8595-pdf-password
Browse files Browse the repository at this point in the history
Frenkield 8595 pdf password
  • Loading branch information
tgolen authored Sep 5, 2022
2 parents e4dd014 + bfbf6f4 commit dc6a69a
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 121 deletions.
8 changes: 8 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,14 @@ const CONST = {
// When generating a random value to fit in 7 digits (for the `middle` or `right` parts above), this is the maximum value to multiply by Math.random().
MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000,
IOS_KEYBOARD_SPACE_OFFSET: -30,

PDF_PASSWORD_FORM: {
// Constants for password-related error responses received from react-pdf.
REACT_PDF_PASSWORD_RESPONSES: {
NEED_PASSWORD: 1,
INCORRECT_PASSWORD: 2,
},
},
};

export default CONST;
62 changes: 49 additions & 13 deletions src/components/AttachmentModal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {View, Animated} from 'react-native';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import lodashExtend from 'lodash/extend';
Expand All @@ -9,6 +9,7 @@ import CONST from '../CONST';
import Modal from './Modal';
import AttachmentView from './AttachmentView';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import themeColors from '../styles/themes/default';
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
import compose from '../libs/compose';
Expand Down Expand Up @@ -78,11 +79,14 @@ class AttachmentModal extends PureComponent {
file: null,
sourceURL: props.sourceURL,
modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
isConfirmButtonDisabled: false,
confirmButtonFadeAnimation: new Animated.Value(1),
};

this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
}

/**
Expand Down Expand Up @@ -123,8 +127,9 @@ class AttachmentModal extends PureComponent {
* Execute the onConfirm callback and close the modal.
*/
submitAndClose() {
// If the modal has already been closed, don't allow another submission
if (!this.state.isModalOpen) {
// If the modal has already been closed or the confirm button is disabled
// do not submit.
if (!this.state.isModalOpen || this.state.isConfirmButtonDisabled) {
return;
}

Expand Down Expand Up @@ -205,14 +210,38 @@ class AttachmentModal extends PureComponent {
}
}

/**
* In order to gracefully hide/show the confirm button when the keyboard
* opens/closes, apply an animation to fade the confirm button out/in. And since
* we're only updating the opacity of the confirm button, we must also conditionally
* disable it.
*
* @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in.
*/
updateConfirmButtonVisibility(shouldFadeOut) {
this.setState({isConfirmButtonDisabled: shouldFadeOut});
const toValue = shouldFadeOut ? 0 : 1;

Animated.timing(this.state.confirmButtonFadeAnimation, {
toValue,
duration: 100,
useNativeDriver: true,
}).start();
}

render() {
const sourceURL = this.props.isAuthTokenRequired
? addEncryptedAuthTokenToURL(this.state.sourceURL)
: this.state.sourceURL;

// When the confirm button is visible we don't need bottom padding on the attachment view.
const attachmentViewPaddingStyles = this.props.onConfirm
? [styles.pl5, styles.pr5, styles.pt5]
: styles.p5;

const attachmentViewStyles = this.props.isSmallScreenWidth || this.props.isMediumScreenWidth
? [styles.imageModalImageCenterContainer]
: [styles.imageModalImageCenterContainer, styles.p5];
: [styles.imageModalImageCenterContainer, attachmentViewPaddingStyles];

const {fileName, fileExtension} = this.splitExtensionFromFileName(this.props.originalFileName || lodashGet(this.state, 'file.name', ''));

Expand Down Expand Up @@ -246,20 +275,27 @@ class AttachmentModal extends PureComponent {
/>
<View style={attachmentViewStyles}>
{this.state.sourceURL && (
<AttachmentView sourceURL={sourceURL} file={this.state.file} />
<AttachmentView
sourceURL={sourceURL}
file={this.state.file}
onToggleKeyboard={this.updateConfirmButtonVisibility}
/>
)}
</View>

{/* If we have an onConfirm method show a confirmation button */}
{this.props.onConfirm && (
<Button
success
style={[styles.buttonConfirm]}
textStyles={[styles.buttonConfirmText]}
text={this.props.translate('common.send')}
onPress={this.submitAndClose}
pressOnEnter
/>
<Animated.View style={StyleUtils.fade(this.state.confirmButtonFadeAnimation)}>
<Button
success
style={[styles.buttonConfirm]}
textStyles={[styles.buttonConfirmText]}
text={this.props.translate('common.send')}
onPress={this.submitAndClose}
disabled={this.state.isConfirmButtonDisabled}
pressOnEnter
/>
</Animated.View>
)}
</Modal>

Expand Down
5 changes: 5 additions & 0 deletions src/components/AttachmentView.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const propTypes = {
/** Flag to show the loading indicator */
shouldShowLoadingSpinnerIcon: PropTypes.bool,

/** Notify parent that the UI should be modified to accommodate keyboard */
onToggleKeyboard: PropTypes.func,

...withLocalizePropTypes,
};

Expand All @@ -37,6 +40,7 @@ const defaultProps = {
},
shouldShowDownloadIcon: false,
shouldShowLoadingSpinnerIcon: false,
onToggleKeyboard: () => {},
};

const AttachmentView = (props) => {
Expand All @@ -48,6 +52,7 @@ const AttachmentView = (props) => {
<PDFView
sourceURL={props.sourceURL}
style={styles.imageModalPDF}
onToggleKeyboard={props.onToggleKeyboard}
/>
);
}
Expand Down
43 changes: 43 additions & 0 deletions src/components/PDFView/PDFInfoMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import Text from '../Text';
import TextLink from '../TextLink';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import styles from '../../styles/styles';
import variables from '../../styles/variables';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';

const propTypes = {
/** Callback function to indicate that PDF password form should be shown */
onShowForm: PropTypes.func.isRequired,

...withLocalizePropTypes,
};

const PDFInfoMessage = props => (
<View style={styles.alignItemsCenter}>
<Icon
src={Expensicons.EyeDisabled}
width={variables.iconSizeSuperLarge}
height={variables.iconSizeSuperLarge}
/>
<Text style={[styles.h1, styles.mb3, styles.mt3]}>
{props.translate('attachmentView.pdfPasswordForm.title')}
</Text>
<Text>{props.translate('attachmentView.pdfPasswordForm.infoText')}</Text>
<Text>
{props.translate('attachmentView.pdfPasswordForm.beforeLinkText')}
<TextLink onPress={props.onShowForm}>
{` ${props.translate('attachmentView.pdfPasswordForm.linkText')} `}
</TextLink>
{props.translate('attachmentView.pdfPasswordForm.afterLinkText')}
</Text>
</View>
);

PDFInfoMessage.propTypes = propTypes;
PDFInfoMessage.displayName = 'PDFInfoMessage';

export default withLocalize(PDFInfoMessage);
163 changes: 163 additions & 0 deletions src/components/PDFView/PDFPasswordForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import _ from 'underscore';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {View, ScrollView} from 'react-native';
import Button from '../Button';
import Text from '../Text';
import TextInput from '../TextInput';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import styles from '../../styles/styles';
import colors from '../../styles/colors';
import PDFInfoMessage from './PDFInfoMessage';
import compose from '../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';

const propTypes = {
/** If the submitted password is invalid (show an error message) */
isPasswordInvalid: PropTypes.bool,

/** If the password field should be auto-focused */
shouldAutofocusPasswordField: PropTypes.bool,

/** If loading indicator should be shown */
shouldShowLoadingIndicator: PropTypes.bool,

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

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

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

...withLocalizePropTypes,
...windowDimensionsPropTypes,
};

const defaultProps = {
isPasswordInvalid: false,
shouldAutofocusPasswordField: false,
shouldShowLoadingIndicator: false,
onSubmit: () => {},
onPasswordUpdated: () => {},
onPasswordFieldFocused: () => {},
};

class PDFPasswordForm extends Component {
constructor(props) {
super(props);
this.state = {
password: '',
validationErrorText: '',
shouldShowForm: false,
};
this.submitPassword = this.submitPassword.bind(this);
this.updatePassword = this.updatePassword.bind(this);
this.showForm = this.showForm.bind(this);
this.validateAndNotifyPasswordBlur = this.validateAndNotifyPasswordBlur.bind(this);
}

submitPassword() {
if (!this.validate()) {
return;
}
this.props.onSubmit(this.state.password);
}

updatePassword(password) {
this.props.onPasswordUpdated(password);
if (!_.isEmpty(password) && this.state.validationErrorText) {
this.setState({validationErrorText: ''});
}
this.setState({password});
}

validate() {
if (!_.isEmpty(this.state.password)) {
return true;
}
this.setState({
validationErrorText: this.props.translate('attachmentView.passwordRequired'),
});
return false;
}

validateAndNotifyPasswordBlur() {
this.validate();
this.props.onPasswordFieldFocused(false);
}

showForm() {
this.setState({shouldShowForm: true});
}

render() {
const containerStyle = this.props.isSmallScreenWidth
? [styles.flex1, styles.w100]
: styles.pdfPasswordForm.wideScreenWidth;

return (
<>
{this.state.shouldShowForm ? (
<ScrollView
keyboardShouldPersistTaps="handled"
style={containerStyle}
contentContainerStyle={[styles.ph5, styles.flex1, styles.justifyContentCenter]}
>
<View style={styles.mb4}>
<Text>
{this.props.translate('attachmentView.pdfPasswordForm.formLabel')}
</Text>
</View>
<TextInput
label={this.props.translate('common.password')}
autoComplete="off"
autoCorrect={false}
textContentType="password"
onChangeText={this.updatePassword}
returnKeyType="done"
onSubmitEditing={this.submitPassword}
errorText={this.state.validationErrorText}
onFocus={() => this.props.onPasswordFieldFocused(true)}
onBlur={this.validateAndNotifyPasswordBlur}
autoFocus={this.props.shouldAutofocusPasswordField}
secureTextEntry
/>
{this.props.isPasswordInvalid && (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt3]}>
<Icon src={Expensicons.Exclamation} fill={colors.red} />
<View style={[styles.flexRow, styles.ml2, styles.flexWrap, styles.flex1]}>
<Text style={styles.mutedTextLabel}>
{this.props.translate('attachmentView.passwordIncorrect')}
</Text>
</View>
</View>
)}
<Button
text={this.props.translate('common.confirm')}
onPress={this.submitPassword}
style={styles.pt4}
isLoading={this.props.shouldShowLoadingIndicator}
pressOnEnter
/>
</ScrollView>
) : (
<View style={[styles.flex1, styles.justifyContentCenter]}>
<PDFInfoMessage onShowForm={this.showForm} />
</View>
)}
</>
);
}
}

PDFPasswordForm.propTypes = propTypes;
PDFPasswordForm.defaultProps = defaultProps;

export default compose(
withWindowDimensions,
withLocalize,
)(PDFPasswordForm);
Loading

0 comments on commit dc6a69a

Please sign in to comment.