-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New Feature: Add Split by percentage to the split expense flow for new splits
#74652
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
Merged
lakchote
merged 48 commits into
Expensify:main
from
ikevin127:ikevin127-splitEvenlyPercentage
Dec 18, 2025
Merged
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
d0701e6
Add 'Split by percentage' to the split expense table for new splits
ikevin127 8a1a1a9
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 9930a8b
added illustration, translations, refactoring and tests
ikevin127 c8886b6
svg compression
ikevin127 482d706
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 ec4741a
UI adjustments
ikevin127 2a42e53
fixed ios native style issue
ikevin127 ec52bcb
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 ae35348
submodule sync
ikevin127 543a930
perf-6 improvements - ready for review
ikevin127 9151325
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 a52ea8c
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 1adff7a
resolved review comments
ikevin127 f21c079
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 fc60538
eslint
ikevin127 b088aef
resolved review comments (2)
ikevin127 31e28e2
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 0eaaa23
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 b8618fb
resolved review comments (3)
ikevin127 7b59898
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 b8cefad
fixed eslint import
ikevin127 1c774d2
resolved review comments (4)
ikevin127 de1a28a
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 950366a
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 3569746
eslint
ikevin127 7759f8f
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 99b70e5
resolved review comments (5)
ikevin127 f45d2e6
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 532567a
addressed spacing and flicker
ikevin127 c502ba8
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 a3e136e
Revert to Option C, added 0.1% precision logic
ikevin127 8819b29
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 b154078
resolved submodule
ikevin127 905a9c6
resolved submodule (1)
ikevin127 f565927
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 1c7503b
added JSDoc comments to new components
ikevin127 5e4425e
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 57441de
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 5bd0d3f
resolve submodule
ikevin127 dc0ebcf
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 94cb4d6
addressed focus scrolling inconsistencies
ikevin127 cee1a11
resolved submodule
ikevin127 d71f378
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 6bc9540
onyx tab navigator refactoring
ikevin127 2aedb86
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 5bd8bcf
fix: navigation go back - not found
ikevin127 109bf29
fix: typecheck
ikevin127 c3df553
fix: react compiler
ikevin127 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/components/SelectionListWithSections/SplitExpense/SplitAmountDisplay.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import React, {useState} from 'react'; | ||
| import {View} from 'react-native'; | ||
| import type {SplitListItemType} from '@components/SelectionListWithSections/types'; | ||
| import Text from '@components/Text'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; | ||
| import CONST from '@src/CONST'; | ||
|
|
||
| type SplitAmountDisplayProps = { | ||
| /** The split item data containing amount, currency, and editable state. */ | ||
| splitItem: SplitListItemType; | ||
| /** The width of the content area. */ | ||
| contentWidth?: number | string; | ||
| /** Whether to remove default spacing from the container. */ | ||
| shouldRemoveSpacing?: boolean; | ||
| }; | ||
|
|
||
| function SplitAmountDisplay({splitItem, contentWidth = '100%', shouldRemoveSpacing = false}: SplitAmountDisplayProps) { | ||
| const styles = useThemeStyles(); | ||
| const [prefixCharacterMargin, setPrefixCharacterMargin] = useState<number>(CONST.CHARACTER_WIDTH); | ||
|
|
||
| return ( | ||
| <View style={[styles.cannotBeEditedSplitInputContainer, shouldRemoveSpacing && [styles.removeSpacing]]}> | ||
| <Text | ||
| style={[styles.optionRowAmountInput, styles.pAbsolute]} | ||
| onLayout={(event) => { | ||
| if (event.nativeEvent.layout.width === 0 && event.nativeEvent.layout.height === 0) { | ||
| return; | ||
| } | ||
| setPrefixCharacterMargin(event.nativeEvent.layout.width); | ||
| }} | ||
| > | ||
| {splitItem.currencySymbol} | ||
| </Text> | ||
| <Text | ||
| style={[styles.getSplitListItemAmountStyle(prefixCharacterMargin, contentWidth), styles.textAlignLeft]} | ||
| numberOfLines={1} | ||
| > | ||
| {convertToDisplayStringWithoutCurrency(splitItem.amount, splitItem.currency)} | ||
| </Text> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| SplitAmountDisplay.displayName = 'SplitAmountDisplay'; | ||
|
|
||
| export default SplitAmountDisplay; | ||
ikevin127 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
70 changes: 70 additions & 0 deletions
70
src/components/SelectionListWithSections/SplitExpense/SplitAmountInput.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import React from 'react'; | ||
| import type {BlurEvent} from 'react-native'; | ||
| import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; | ||
| import type {SplitListItemType} from '@components/SelectionListWithSections/types'; | ||
| import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import SplitAmountDisplay from './SplitAmountDisplay'; | ||
|
|
||
| type SplitAmountInputProps = { | ||
| /** The split item data containing amount, currency, and editable state. */ | ||
| splitItem: SplitListItemType; | ||
| /** The formatted original amount string used to calculate max input length. */ | ||
| formattedOriginalAmount: string; | ||
| /** The width of the input content area. */ | ||
| contentWidth: number; | ||
| /** Callback invoked when the split expense value changes. */ | ||
| onSplitExpenseValueChange: (value: string) => void; | ||
| /** Callback invoked when the input receives focus. */ | ||
| focusHandler: () => void; | ||
| /** Callback invoked when the input loses focus. */ | ||
| onInputBlur: ((e: BlurEvent) => void) | undefined; | ||
| /** Callback ref for accessing the underlying text input. */ | ||
| inputCallbackRef: (ref: BaseTextInputRef | null) => void; | ||
| }; | ||
|
|
||
| function SplitAmountInput({splitItem, formattedOriginalAmount, contentWidth, onSplitExpenseValueChange, focusHandler, onInputBlur, inputCallbackRef}: SplitAmountInputProps) { | ||
| const styles = useThemeStyles(); | ||
|
|
||
| if (splitItem.isEditable) { | ||
| return ( | ||
| <MoneyRequestAmountInput | ||
| ref={inputCallbackRef} | ||
| disabled={!splitItem.isEditable} | ||
| autoGrow={false} | ||
| amount={splitItem.amount} | ||
| currency={splitItem.currency} | ||
| prefixCharacter={splitItem.currencySymbol} | ||
| disableKeyboard={false} | ||
| isCurrencyPressable={false} | ||
| hideFocusedState={false} | ||
| hideCurrencySymbol | ||
| submitBehavior="blurAndSubmit" | ||
| formatAmountOnBlur | ||
| onAmountChange={onSplitExpenseValueChange} | ||
| prefixContainerStyle={[styles.pv0, styles.h100]} | ||
| prefixStyle={styles.lineHeightUndefined} | ||
| inputStyle={[styles.optionRowAmountInput, styles.lineHeightUndefined]} | ||
| containerStyle={[styles.textInputContainer, styles.pl2, styles.pr1]} | ||
| touchableInputWrapperStyle={[styles.ml3]} | ||
| maxLength={formattedOriginalAmount.length + 1} | ||
| contentWidth={contentWidth} | ||
| shouldApplyPaddingToContainer | ||
| shouldUseDefaultLineHeightForPrefix={false} | ||
| shouldWrapInputInContainer={false} | ||
| onFocus={focusHandler} | ||
| onBlur={onInputBlur} | ||
| /> | ||
| ); | ||
| } | ||
| return ( | ||
| <SplitAmountDisplay | ||
| splitItem={splitItem} | ||
| contentWidth={contentWidth} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| SplitAmountInput.displayName = 'SplitAmountInput'; | ||
|
|
||
| export default SplitAmountInput; | ||
ikevin127 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
32 changes: 32 additions & 0 deletions
32
src/components/SelectionListWithSections/SplitExpense/SplitPercentageDisplay.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import type {SplitListItemType} from '@components/SelectionListWithSections/types'; | ||
| import Text from '@components/Text'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import CONST from '@src/CONST'; | ||
|
|
||
| type SplitPercentageDisplayProps = { | ||
| /** The split item data containing amount, currency, and editable state. */ | ||
| splitItem: SplitListItemType; | ||
| /** The width of the content area. */ | ||
| contentWidth: number; | ||
| }; | ||
|
|
||
| function SplitPercentageDisplay({splitItem, contentWidth}: SplitPercentageDisplayProps) { | ||
| const styles = useThemeStyles(); | ||
|
|
||
| return ( | ||
| <View style={[styles.cannotBeEditedSplitInputContainer, styles.ph0]}> | ||
| <Text | ||
| style={[styles.getSplitListItemAmountStyle(CONST.CHARACTER_WIDTH, contentWidth), styles.textAlignLeft]} | ||
| numberOfLines={1} | ||
| > | ||
| {splitItem.percentage}% | ||
| </Text> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| SplitPercentageDisplay.displayName = 'SplitPercentageDisplay'; | ||
|
|
||
| export default SplitPercentageDisplay; | ||
ikevin127 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
65 changes: 65 additions & 0 deletions
65
src/components/SelectionListWithSections/SplitExpense/SplitPercentageInput.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import React from 'react'; | ||
| import type {BlurEvent} from 'react-native'; | ||
| import PercentageForm from '@components/PercentageForm'; | ||
| import type {SplitListItemType} from '@components/SelectionListWithSections/types'; | ||
| import useStyleUtils from '@hooks/useStyleUtils'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import SplitPercentageDisplay from './SplitPercentageDisplay'; | ||
|
|
||
| type SplitPercentageInputProps = { | ||
| /** The split item data containing amount, currency, and editable state. */ | ||
| splitItem: SplitListItemType; | ||
| /** The width of the input content area. */ | ||
| contentWidth: number; | ||
| /** The draft percentage value while the user is editing. */ | ||
| percentageDraft: string | undefined; | ||
| /** Callback invoked when the split expense value changes. */ | ||
| onSplitExpenseValueChange: (value: string) => void; | ||
| /** State setter for the percentage draft value. */ | ||
| setPercentageDraft: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| /** Callback invoked when the input receives focus. */ | ||
| focusHandler: () => void; | ||
| /** Callback invoked when the input loses focus. */ | ||
| onInputBlur: ((e: BlurEvent) => void) | undefined; | ||
| }; | ||
|
|
||
| function SplitPercentageInput({splitItem, contentWidth, percentageDraft, onSplitExpenseValueChange, setPercentageDraft, focusHandler, onInputBlur}: SplitPercentageInputProps) { | ||
| const styles = useThemeStyles(); | ||
| const StyleUtils = useStyleUtils(); | ||
|
|
||
| const inputValue = percentageDraft ?? String(splitItem.percentage ?? 0); | ||
|
|
||
| if (splitItem.isEditable) { | ||
| return ( | ||
| <PercentageForm | ||
| onInputChange={(value) => { | ||
| setPercentageDraft(value); | ||
| onSplitExpenseValueChange(value); | ||
| }} | ||
| value={inputValue} | ||
| textInputContainerStyles={StyleUtils.splitPercentageInputStyles(styles)} | ||
| containerStyles={styles.optionRowPercentInputContainer} | ||
| inputStyle={[styles.optionRowPercentInput, styles.lineHeightUndefined]} | ||
| onFocus={focusHandler} | ||
| onBlur={(event) => { | ||
| setPercentageDraft(undefined); | ||
| if (onInputBlur) { | ||
| onInputBlur(event); | ||
| } | ||
| }} | ||
| allowExceedingHundred | ||
| allowDecimal | ||
| /> | ||
| ); | ||
| } | ||
| return ( | ||
| <SplitPercentageDisplay | ||
| splitItem={splitItem} | ||
| contentWidth={contentWidth} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| SplitPercentageInput.displayName = 'SplitPercentageInput'; | ||
|
|
||
| export default SplitPercentageInput; | ||
ikevin127 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this logic out of this component in some way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason to change the viewOffset here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing scroll offset for percentage mode on native platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you suggest we do that differently if we need to remove
variables.contentHeaderHeighton native for whenisPercentageMode?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as other