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

Create IOU Amount step #1768

Merged
merged 11 commits into from
Mar 18, 2021
75 changes: 75 additions & 0 deletions src/components/BigNumberPad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, {PureComponent} from 'react';
import {
Text, TouchableOpacity, View,
} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles/styles';

const propTypes = {
// Callback to inform parent modal with key pressed
numberPressed: PropTypes.func.isRequired,
tugbadogan marked this conversation as resolved.
Show resolved Hide resolved
};

const padNumbers = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['.', '0', '<'],
Julesssss marked this conversation as resolved.
Show resolved Hide resolved
];

class BigNumberPad extends PureComponent {
tugbadogan marked this conversation as resolved.
Show resolved Hide resolved
/**
* Creates set of buttons for given row
*
* @param {number} row
* @returns {View}
*/
createNumberPadRow(row) {
const self = this;
const numberPadRow = padNumbers[row].map((column, index) => self.createNumberPadButton(row, index));
return (
<View key={row} style={[styles.flexRow, styles.mt3]}>
{numberPadRow}
</View>
);
}

/**
* Creates a button for given row and column
*
* @param {number} row
* @param {number} column
* @returns {View}
*/
createNumberPadButton(row, column) {
// Adding margin between buttons except first column to
// avoid unccessary space before the first column.
const marginLeft = column > 0 ? styles.ml3 : {};
return (
<TouchableOpacity
key={padNumbers[row][column]}
style={[styles.flex1, styles.button, marginLeft]}
onPress={() => this.props.numberPressed(padNumbers[row][column])}
>
<Text style={[styles.buttonText]}>
{padNumbers[row][column]}
</Text>
</TouchableOpacity>
);
}

render() {
const self = this;
const numberPad = padNumbers.map((row, index) => self.createNumberPadRow(index));
return (
<View style={[styles.flexColumn, styles.w100]}>
{numberPad}
</View>
);
}
}

BigNumberPad.propTypes = propTypes;
BigNumberPad.displayName = 'BigNumberPad';

export default BigNumberPad;
14 changes: 14 additions & 0 deletions src/components/TextInputFocusable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class TextInputFocusable extends React.Component {
}
if (prevProps.defaultValue !== this.props.defaultValue) {
this.updateNumberOfLines();
this.moveCursorToEnd();
}
}

Expand Down Expand Up @@ -198,6 +199,19 @@ class TextInputFocusable extends React.Component {
});
}

/**
* Move cursor to end by setting start and end
* to length of the input value.
*/
moveCursorToEnd() {
this.setState({
selection: {
start: this.props.defaultValue.length,
end: this.props.defaultValue.length,
},
});
}

focusInput() {
this.textInput.focus();
}
Expand Down
58 changes: 54 additions & 4 deletions src/pages/iou/IOUModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ class IOUModal extends Component {

this.navigateToPreviousStep = this.navigateToPreviousStep.bind(this);
this.navigateToNextStep = this.navigateToNextStep.bind(this);
this.updateAmount = this.updateAmount.bind(this);
this.currencySelected = this.currencySelected.bind(this);

this.addParticipants = this.addParticipants.bind(this);
this.state = {
currentStepIndex: 0,
participants: [],
iouAmount: 42,
amount: '',
selectedCurrency: 'USD',
isAmountPageNextButtonDisabled: true,
};
}

Expand All @@ -60,7 +64,10 @@ class IOUModal extends Component {

getTitleForStep() {
if (this.state.currentStepIndex === 1) {
return `${this.props.hasMultipleParticipants ? 'Split' : 'Request'} $${this.state.iouAmount}`;
return `${this.props.hasMultipleParticipants ? 'Split' : 'Request'} $${this.state.amount}`;
}
if (steps[this.state.currentStepIndex] === Steps.IOUAmount) {
return this.props.hasMultipleParticipants ? 'Split Bill' : 'Request Money';
}
return steps[this.state.currentStepIndex] || '';
}
Expand Down Expand Up @@ -95,6 +102,42 @@ class IOUModal extends Component {
});
}

/**
* Update amount with number or Backspace pressed.
* Validate new amount with decimal number regex up to 6 digits and 2 decimal digit
*
* @param {String} buttonPressed
*/
updateAmount(buttonPressed) {
// Backspace button is pressed
if (buttonPressed === '<' || buttonPressed === 'Backspace') {
if (this.state.amount.length > 0) {
this.setState(prevState => ({
amount: prevState.amount.substring(0, prevState.amount.length - 1),
isAmountPageNextButtonDisabled: prevState.amount.length === 1,
}));
}
} else {
const decimalNumberRegex = new RegExp(/^\d{1,6}(\.\d{0,2})?$/, 'i');
Julesssss marked this conversation as resolved.
Show resolved Hide resolved
const amount = this.state.amount + buttonPressed;
if (decimalNumberRegex.test(amount)) {
this.setState({
amount,
isAmountPageNextButtonDisabled: false,
});
}
}
}

/**
* Update the currency state
*
* @param {String} selectedCurrency
*/
currencySelected(selectedCurrency) {
this.setState({selectedCurrency});
}

render() {
const currentStep = steps[this.state.currentStepIndex];
return (
Expand Down Expand Up @@ -130,7 +173,14 @@ class IOUModal extends Component {
</View>
</View>
{currentStep === Steps.IOUAmount && (
<IOUAmountPage onStepComplete={this.navigateToNextStep} />
<IOUAmountPage
onStepComplete={this.navigateToNextStep}
numberPressed={this.updateAmount}
currencySelected={this.currencySelected}
amount={this.state.amount}
selectedCurrency={this.state.selectedCurrency}
isNextButtonDisabled={this.state.isAmountPageNextButtonDisabled}
/>
)}
{currentStep === Steps.IOUParticipants && (
<IOUParticipantsPage
Expand All @@ -144,7 +194,7 @@ class IOUModal extends Component {
<IOUConfirmPage
onConfirm={() => console.debug('create IOU report')}
participants={this.state.participants}
iouAmount={42}
iouAmount={this.state.amount}
/>
)}
</>
Expand Down
110 changes: 95 additions & 15 deletions src/pages/iou/steps/IOUAmountPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,33 @@ import {withOnyx} from 'react-native-onyx';
import ONYXKEYS from '../../../ONYXKEYS';
import styles from '../../../styles/styles';
import themeColors from '../../../styles/themes/default';
import BigNumberPad from '../../../components/BigNumberPad';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
tugbadogan marked this conversation as resolved.
Show resolved Hide resolved
import TextInputFocusable from '../../../components/TextInputFocusable';

const propTypes = {
// Callback to inform parent modal of success
onStepComplete: PropTypes.func.isRequired,

// Callback to inform parent modal with key pressed
numberPressed: PropTypes.func.isRequired,

// Currency selection will be implemented later
// eslint-disable-next-line react/no-unused-prop-types
currencySelected: PropTypes.func.isRequired,

// User's currency preference
selectedCurrency: PropTypes.string.isRequired,

// Amount value entered by user
amount: PropTypes.string.isRequired,

// To disable/enable Next button based on amount validity
isNextButtonDisabled: PropTypes.bool.isRequired,

/* Window Dimensions Props */
...windowDimensionsPropTypes,

/* Onyx Props */

// Holds data related to IOU view state, rather than the underlying IOU data.
Expand All @@ -28,25 +50,83 @@ const propTypes = {
const defaultProps = {
iou: {},
};
class IOUAmountPage extends React.Component {
constructor(props) {
super(props);

this.state = {
textInputWidth: 0,
};
}

const IOUAmountPage = props => (
<View style={styles.pageWrapper}>
{props.iou.loading && <ActivityIndicator color={themeColors.text} />}
<TouchableOpacity
style={[styles.button, styles.w100, styles.mt5]}
onPress={props.onStepComplete}
>
<Text style={[styles.buttonText]}>
Next
</Text>
</TouchableOpacity>
</View>
);
componentDidMount() {
if (this.textInput) {
this.textInput.focus();
}
}

render() {
return (
<View style={[styles.flex1, styles.pageWrapper]}>
{this.props.iou.loading && <ActivityIndicator color={themeColors.text} />}
<View style={[
styles.flex1,
styles.flexRow,
styles.w100,
styles.alignItemsCenter,
styles.justifyContentCenter,
]}
>
<Text style={styles.iouAmountText}>
{this.props.selectedCurrency}
</Text>
{this.props.isSmallScreenWidth
? <Text style={styles.iouAmountText}>{this.props.amount}</Text>
: (
<View>
<TextInputFocusable
style={[styles.iouAmountTextInput,
{width: Math.max(5, this.state.textInputWidth)}]}
onKeyPress={(event) => {
this.props.numberPressed(event.key);
event.preventDefault();
}}
ref={el => this.textInput = el}
defaultValue={this.props.amount}
textAlign="left"
/>
<Text
style={[styles.iouAmountText, styles.invisible, {left: 100000}]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use inline styles like {left: 100000} directly in the style tag.
Also, why 100000? It looks maybe like a magic number?

onLayout={e => this.setState({textInputWidth: e.nativeEvent.layout.width})}
>
{this.props.amount}
</Text>
</View>
)}
</View>
<View style={[styles.w100, styles.justifyContentEnd]}>
{this.props.isSmallScreenWidth
? <BigNumberPad numberPressed={this.props.numberPressed} />
: <View />}
<TouchableOpacity
style={[styles.button, styles.w100, styles.mt5, styles.buttonSuccess,
this.props.isNextButtonDisabled ? styles.buttonSuccessDisabled : {}]}
onPress={this.props.onStepComplete}
disabled={this.props.isNextButtonDisabled}
>
<Text style={[styles.buttonText, styles.buttonSuccessText]}>
Next
</Text>
</TouchableOpacity>
</View>
</View>
);
}
}
IOUAmountPage.displayName = 'IOUAmountPage';
IOUAmountPage.propTypes = propTypes;
IOUAmountPage.defaultProps = defaultProps;

export default withOnyx({
export default withWindowDimensions(withOnyx({
iou: {key: ONYXKEYS.IOU},
})(IOUAmountPage);
})(IOUAmountPage));
16 changes: 16 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ const styles = {
borderWidth: 0,
},

buttonSuccessDisabled: {
opacity: 0.5,
},

buttonSuccessHovered: {
backgroundColor: themeColors.buttonSuccessHoveredBG,
borderWidth: 0,
Expand Down Expand Up @@ -1092,6 +1096,18 @@ const styles = {
height: 24,
lineHeight: 20,
},

iouAmountText: {
fontFamily: fontFamily.GTA_BOLD,
fontWeight: fontWeightBold,
fontSize: variables.iouAmountTextSize,
Julesssss marked this conversation as resolved.
Show resolved Hide resolved
},

iouAmountTextInput: addOutlineWidth({
fontFamily: fontFamily.GTA_BOLD,
fontWeight: fontWeightBold,
fontSize: variables.iouAmountTextSize,
}, 0),
};

const baseCodeTagStyles = {
Expand Down
1 change: 1 addition & 0 deletions src/styles/variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
iconSizeSmall: 16,
iconSizeNormal: 20,
iconSizeLarge: 24,
iouAmountTextSize: 40,
mobileResponsiveWidthBreakpoint: 800,
safeInsertPercentage: 0.7,
sideBarWidth: 375,
Expand Down