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

Deselect attendees when splitting bill #3881

Merged
merged 34 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d060afc
define new state
rushatgabhane Jul 3, 2021
ff03921
toggle option b/w selected and unselected
rushatgabhane Jul 3, 2021
13af165
define formatting methods
rushatgabhane Jul 3, 2021
59a466f
format selectedParticipants on component mount
rushatgabhane Jul 3, 2021
15d4c93
add unselected section
rushatgabhane Jul 3, 2021
07e6faf
calculate amt based on selected participants
rushatgabhane Jul 3, 2021
edd87c9
get only selected options
rushatgabhane Jul 3, 2021
2ee299c
no row interactivity for single participant
rushatgabhane Jul 3, 2021
a1ef4c1
refactor, disable button on empty selection
rushatgabhane Jul 3, 2021
4f857a5
fix formatting for single participant
rushatgabhane Jul 4, 2021
e48b5b6
add createIOUSplitGroup()
rushatgabhane Jul 4, 2021
4bc85ee
cleanup
rushatgabhane Jul 4, 2021
231f9f7
call iouSplitGroup when split happens from a group
rushatgabhane Jul 4, 2021
ba82754
eslint
rushatgabhane Jul 4, 2021
c537b3d
cleanup
rushatgabhane Jul 5, 2021
aacbecb
capitalize comment
rushatgabhane Jul 8, 2021
028cc17
set split initiator as default user
rushatgabhane Jul 9, 2021
460c269
refactor
rushatgabhane Jul 9, 2021
1f17bff
fix comment style
rushatgabhane Jul 11, 2021
fe09e56
rm unused function
rushatgabhane Jul 11, 2021
06f79e8
change to one state
rushatgabhane Jul 11, 2021
8354514
rename formatting functions
rushatgabhane Jul 11, 2021
a0a38e5
fix comment style
rushatgabhane Jul 11, 2021
8105590
change to single state
rushatgabhane Jul 11, 2021
b997ce3
use setState callback for referencing prevState
rushatgabhane Jul 11, 2021
124e70c
eslint
rushatgabhane Jul 11, 2021
7590bff
refactor
rushatgabhane Jul 12, 2021
dc27691
refactor
rushatgabhane Jul 12, 2021
8ee203f
create new section for unselected participants
rushatgabhane Jul 12, 2021
2838480
use _.map, _.filter instead of native methods
rushatgabhane Jul 12, 2021
257213b
move regex to CONST.REGEX
rushatgabhane Jul 13, 2021
c730c42
add login prop to logged in user
rushatgabhane Jul 13, 2021
7fb8da8
subscribe to session, refactor check for self
rushatgabhane Jul 13, 2021
ecb0d48
fix merge conflicts
rushatgabhane Jul 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.js

Large diffs are not rendered by default.

153 changes: 116 additions & 37 deletions src/components/IOUConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {ScrollView, TextInput} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import _ from 'underscore';
import styles from '../styles/styles';
import Text from './Text';
import themeColors from '../styles/themes/default';
Expand Down Expand Up @@ -101,41 +102,97 @@ const defaultProps = {
const MINIMUM_BOTTOM_OFFSET = 240;

class IOUConfirmationList extends Component {
constructor(props) {
super(props);

this.toggleOption = this.toggleOption.bind(this);

const formattedParticipants = _.map(this.getParticipantsWithAmount(this.props.participants), participant => ({
...participant, selected: true,
}));

this.state = {
participants: formattedParticipants,
};
}

/**
* Get selected participants
* @returns {Array}
*/
getSelectedParticipants() {
return _.filter(this.state.participants, participant => participant.selected);
}

/**
* Get unselected participants
* @returns {Array}
*/
getUnselectedParticipants() {
return _.filter(this.state.participants, participant => !participant.selected);
}

/**
* Returns the participants with amount
* @param {Array} participants
* @returns {Array}
*/
getParticipantsWithAmount(participants) {
return getIOUConfirmationOptionsFromParticipants(
participants,
this.props.numberFormat(this.calculateAmount(participants) / 100, {
style: 'currency',
currency: this.props.selectedCurrency.currencyCode,
}),
);
}

/**
* Returns the participants without amount
* @param {Array} participants
* @returns {Array}
*/
getParticipantsWithoutAmount(participants) {
return _.map(participants, option => _.omit(option, 'descriptiveText'));
}

/**
* Returns the sections needed for the OptionsSelector
*
* @param {Boolean} maxParticipantsReached
* @returns {Array}
*/
getSections() {
const sections = [];

if (this.props.hasMultipleParticipants) {
const selectedParticipants = this.getSelectedParticipants();
const unselectedParticipants = this.getUnselectedParticipants();

const formattedSelectedParticipants = this.getParticipantsWithAmount(selectedParticipants);
const formattedUnselectedParticipants = this.getParticipantsWithoutAmount(unselectedParticipants);

const formattedMyPersonalDetails = getIOUConfirmationOptionsFromMyPersonalDetail(
this.props.myPersonalDetails,
this.props.numberFormat(this.calculateAmount() / 100, {
this.props.numberFormat(this.calculateAmount(selectedParticipants, true) / 100, {
style: 'currency',
currency: this.props.selectedCurrency.currencyCode,
}),
);

const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants,
this.props.numberFormat(this.calculateAmount() / 100, {
style: 'currency',
currency: this.props.selectedCurrency.currencyCode,
}));

sections.push({
title: this.props.translate('iOUConfirmationList.whoPaid'),
data: [formattedMyPersonalDetails],
shouldShow: true,
indexOffset: 0,
});
sections.push({
}, {
title: this.props.translate('iOUConfirmationList.whoWasThere'),
data: formattedParticipants,
data: formattedSelectedParticipants,
shouldShow: true,
indexOffset: 0,
}, {
title: undefined,
data: formattedUnselectedParticipants,
shouldShow: !_.isEmpty(formattedUnselectedParticipants),
indexOffset: 0,
});
} else {
const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants,
Expand All @@ -156,7 +213,6 @@ class IOUConfirmationList extends Component {

/**
* Gets splits for the transaction
*
* @returns {Array|null}
*/
getSplits() {
Expand All @@ -165,63 +221,54 @@ class IOUConfirmationList extends Component {
if (!this.props.hasMultipleParticipants) {
return null;
}

const splits = this.props.participants.map(participant => ({
const selectedParticipants = this.getSelectedParticipants();
const splits = _.map(selectedParticipants, participant => ({
email: participant.login,

// We should send in cents to API
// Cents is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(),
amount: this.calculateAmount(selectedParticipants),
}));

splits.push({
email: this.props.myPersonalDetails.login,

// The user is default and we should send in cents to API
// USD is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(true),
amount: this.calculateAmount(selectedParticipants, true),
});
return splits;
}

/**
* Gets participants list for a report
*
* Returns selected options -- there is checkmark for every row in List for split flow
* @returns {Array}
*/
getParticipants() {
const participants = this.props.participants.map(participant => participant.login);
participants.push(this.props.myPersonalDetails.login);
return participants;
}

/**
* Returns selected options with all participant logins -- there is checkmark for every row in List for split flow
* @returns {Array}
*/
getAllOptionsAsSelected() {
getSelectedOptions() {
if (!this.props.hasMultipleParticipants) {
return [];
}
const selectedParticipants = this.getSelectedParticipants();
return [
...this.props.participants,
...selectedParticipants,
getIOUConfirmationOptionsFromMyPersonalDetail(this.props.myPersonalDetails),
];
}

/**
* Calculates the amount per user
* Calculates the amount per user given a list of participants
* @param {Array} participants
* @param {Boolean} isDefaultUser
* @returns {Number}
*/
calculateAmount(isDefaultUser = false) {
calculateAmount(participants, isDefaultUser = false) {
rushatgabhane marked this conversation as resolved.
Show resolved Hide resolved
// Convert to cents before working with iouAmount to avoid
// javascript subtraction with decimal problem -- when dealing with decimals,
// because they are encoded as IEEE 754 floating point numbers, some of the decimal
// numbers cannot be represented with perfect accuracy.
// Cents is temporary and there must be support for other currencies in the future
const iouAmount = Math.round(parseFloat(this.props.iouAmount * 100));
const totalParticipants = this.props.participants.length + 1;
const totalParticipants = participants.length + 1;
const amountPerPerson = Math.round(iouAmount / totalParticipants);

if (!isDefaultUser) { return amountPerPerson; }
Expand All @@ -232,6 +279,32 @@ class IOUConfirmationList extends Component {
return iouAmount !== sumAmount ? (amountPerPerson + difference) : amountPerPerson;
}

/**
* Toggle selected option's selected prop.
* @param {Object} option
*/
toggleOption(option) {
rushatgabhane marked this conversation as resolved.
Show resolved Hide resolved
const isSelf = _.every(this.state.participants, selectedOption => (
rushatgabhane marked this conversation as resolved.
Show resolved Hide resolved
selectedOption.login !== option.login
));

if (isSelf) {
return;
}

this.setState((prevState) => {
const newParticipants = _.reject(prevState.participants, participant => (
participant.login === option.login
));

newParticipants.push({
...option,
selected: !option.selected,
});
return {participants: newParticipants};
});
}

render() {
const buttonText = this.props.translate(
this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', {
Expand All @@ -241,6 +314,9 @@ class IOUConfirmationList extends Component {
),
},
);
const hoverStyle = this.props.hasMultipleParticipants ? styles.hoveredComponentBG : {};
const toggleOption = this.props.hasMultipleParticipants ? this.toggleOption : undefined;
const selectedParticipants = this.getSelectedParticipants();
return (
<>
<ScrollView style={[styles.flex1, styles.w100]}>
Expand All @@ -253,12 +329,14 @@ class IOUConfirmationList extends Component {
}]}
sections={this.getSections()}
disableArrowKeysActions
disableRowInteractivity
disableFocusOptions
hideAdditionalOptionStates
forceTextUnreadStyle
canSelectMultipleOptions={this.props.hasMultipleParticipants}
disableFocusOptions
selectedOptions={this.getAllOptionsAsSelected()}
selectedOptions={this.getSelectedOptions()}
onSelectRow={toggleOption}
disableRowInteractivity={!this.props.hasMultipleParticipants}
optionHoveredStyle={hoverStyle}
/>
<Text style={[styles.p5, styles.textMicroBold, styles.colorHeading]}>
{this.props.translate('iOUConfirmationList.whatsItFor')}
Expand All @@ -278,6 +356,7 @@ class IOUConfirmationList extends Component {
success
style={[styles.w100]}
isLoading={this.props.iou.loading}
isDisabled={selectedParticipants.length === 0}
text={buttonText}
onPress={() => this.props.onConfirm(this.getSplits())}
/>
Expand Down
1 change: 1 addition & 0 deletions src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ function getNewGroupOptions(

/**
* Build the options for the Sidebar a.k.a. LHN
*
* @param {Object} reports
* @param {Object} personalDetails
* @param {Object} draftComments
Expand Down
21 changes: 21 additions & 0 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@ function createIOUSplit(params) {
});
}

/**
* Creates IOUSplit Transaction for Group DM
* @param {Object} params
* @param {Array} params.splits
* @param {String} params.comment
* @param {Number} params.amount
* @param {String} params.currency
* @param {String} params.reportID
*/
function createIOUSplitGroup(params) {
Onyx.merge(ONYXKEYS.IOU, {loading: true, creatingIOUTransaction: true, error: false});

API.CreateIOUSplit({
...params,
splits: JSON.stringify(params.splits),
})
.then(() => Onyx.merge(ONYXKEYS.IOU, {loading: false, creatingIOUTransaction: false}))
.catch(() => Onyx.merge(ONYXKEYS.IOU, {error: true}));
}

/**
* Reject an iouReport transaction. Declining and cancelling transactions are done via the same Auth command.
*
Expand Down Expand Up @@ -244,6 +264,7 @@ function payIOUReport({
export {
createIOUTransaction,
createIOUSplit,
createIOUSplitGroup,
rejectTransaction,
payIOUReport,
};
18 changes: 17 additions & 1 deletion src/pages/iou/IOUModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Header from '../../components/Header';
import styles from '../../styles/styles';
import Icon from '../../components/Icon';
import * as PersonalDetails from '../../libs/actions/PersonalDetails';
import {createIOUSplit, createIOUTransaction} from '../../libs/actions/IOU';
import {createIOUSplit, createIOUTransaction, createIOUSplitGroup} from '../../libs/actions/IOU';
import {Close, BackArrow} from '../../components/Icon/Expensicons';
import Navigation from '../../libs/Navigation/Navigation';
import ONYXKEYS from '../../ONYXKEYS';
Expand Down Expand Up @@ -254,6 +254,22 @@ class IOUModal extends Component {
* @param {Array} [splits]
*/
createTransaction(splits) {
const reportID = this.props.route.params.reportID;

// Only splits from a group DM has a reportID
// Check if reportID is a number
if (splits && CONST.REGEX.NUMBER.test(reportID)) {
createIOUSplitGroup({
comment: this.state.comment,

// should send in cents to API
amount: Math.round(this.state.amount * 100),
currency: this.state.selectedCurrency.currencyCode,
splits,
reportID,
});
return;
}
if (splits) {
createIOUSplit({
comment: this.state.comment,
Expand Down