Skip to content

Commit 336e478

Browse files
authored
Merge pull request #48775 from shubham1206agra/missing-personal-details
2 parents 5327409 + 72b265f commit 336e478

30 files changed

+1044
-16
lines changed

src/CONST.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,16 @@ const CONST = {
18251825
VENDOR_BILL: 'bill',
18261826
},
18271827

1828+
MISSING_PERSONAL_DETAILS_INDEXES: {
1829+
MAPPING: {
1830+
LEGAL_NAME: 0,
1831+
DATE_OF_BIRTH: 1,
1832+
ADDRESS: 2,
1833+
PHONE_NUMBER: 3,
1834+
},
1835+
INDEX_LIST: ['1', '2', '3', '4'],
1836+
},
1837+
18281838
ACCOUNT_ID: {
18291839
ACCOUNTING: Number(Config?.EXPENSIFY_ACCOUNT_ID_ACCOUNTING ?? 9645353),
18301840
ADMIN: Number(Config?.EXPENSIFY_ACCOUNT_ID_ADMIN ?? -1),

src/ONYXKEYS.ts

+3
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ const ONYXKEYS = {
557557
DATE_OF_BIRTH_FORM_DRAFT: 'dateOfBirthFormDraft',
558558
HOME_ADDRESS_FORM: 'homeAddressForm',
559559
HOME_ADDRESS_FORM_DRAFT: 'homeAddressFormDraft',
560+
PERSONAL_DETAILS_FORM: 'personalDetailsForm',
561+
PERSONAL_DETAILS_FORM_DRAFT: 'personalDetailsFormDraft',
560562
NEW_ROOM_FORM: 'newRoomForm',
561563
NEW_ROOM_FORM_DRAFT: 'newRoomFormDraft',
562564
ROOM_SETTINGS_FORM: 'roomSettingsForm',
@@ -704,6 +706,7 @@ type OnyxFormValuesMapping = {
704706
[ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm;
705707
[ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: FormTypes.DateOfBirthForm;
706708
[ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.HomeAddressForm;
709+
[ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM]: FormTypes.PersonalDetailsForm;
707710
[ONYXKEYS.FORMS.NEW_ROOM_FORM]: FormTypes.NewRoomForm;
708711
[ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm;
709712
[ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm;

src/ROUTES.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,10 @@ const ROUTES = {
12381238
route: 'restricted-action/workspace/:policyID',
12391239
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
12401240
},
1241+
MISSING_PERSONAL_DETAILS: {
1242+
route: 'missing-personal-details/workspace/:policyID',
1243+
getRoute: (policyID: string) => `missing-personal-details/workspace/${policyID}` as const,
1244+
},
12411245
POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
12421246
route: 'settings/workspaces/:policyID/accounting/netsuite/subsidiary-selector',
12431247
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/subsidiary-selector` as const,

src/SCREENS.ts

+2
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ const SCREENS = {
175175
SETTINGS_CATEGORIES: 'SettingsCategories',
176176
RESTRICTED_ACTION: 'RestrictedAction',
177177
REPORT_EXPORT: 'Report_Export',
178+
MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails',
178179
},
179180
ONBOARDING_MODAL: {
180181
ONBOARDING: 'Onboarding',
@@ -542,6 +543,7 @@ const SCREENS = {
542543
TRANSACTION_RECEIPT: 'TransactionReceipt',
543544
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
544545
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
546+
MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root',
545547
} as const;
546548

547549
type Screen = DeepValueOf<typeof SCREENS>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, {useMemo} from 'react';
2+
import HeaderWithBackButton from '@components/HeaderWithBackButton';
3+
import Modal from '@components/Modal';
4+
import ScreenWrapper from '@components/ScreenWrapper';
5+
import SelectionList from '@components/SelectionList';
6+
import RadioListItem from '@components/SelectionList/RadioListItem';
7+
import useDebouncedState from '@hooks/useDebouncedState';
8+
import useLocalize from '@hooks/useLocalize';
9+
import useThemeStyles from '@hooks/useThemeStyles';
10+
import searchCountryOptions from '@libs/searchCountryOptions';
11+
import type {CountryData} from '@libs/searchCountryOptions';
12+
import StringUtils from '@libs/StringUtils';
13+
import CONST from '@src/CONST';
14+
import type {TranslationPaths} from '@src/languages/types';
15+
16+
type CountrySelectorModalProps = {
17+
/** Whether the modal is visible */
18+
isVisible: boolean;
19+
20+
/** Function to call when the user closes the business type selector modal */
21+
onClose: () => void;
22+
23+
/** Label to display on field */
24+
label: string;
25+
26+
/** Country selected */
27+
currentCountry: string;
28+
29+
/** Function to call when the user selects a country */
30+
onCountrySelected: (value: CountryData) => void;
31+
32+
/** Function to call when the user presses on the modal backdrop */
33+
onBackdropPress?: () => void;
34+
};
35+
36+
function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onClose, label, onBackdropPress}: CountrySelectorModalProps) {
37+
const {translate} = useLocalize();
38+
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
39+
40+
const countries = useMemo(
41+
() =>
42+
Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => {
43+
const countryName = translate(`allCountries.${countryISO}` as TranslationPaths);
44+
return {
45+
value: countryISO,
46+
keyForList: countryISO,
47+
text: countryName,
48+
isSelected: currentCountry === countryISO,
49+
searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
50+
};
51+
}),
52+
[translate, currentCountry],
53+
);
54+
55+
const searchResults = searchCountryOptions(debouncedSearchValue, countries);
56+
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
57+
58+
const styles = useThemeStyles();
59+
60+
return (
61+
<Modal
62+
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
63+
isVisible={isVisible}
64+
onClose={onClose}
65+
onModalHide={onClose}
66+
hideModalContentWhileAnimating
67+
useNativeDriver
68+
onBackdropPress={onBackdropPress}
69+
>
70+
<ScreenWrapper
71+
style={[styles.pb0]}
72+
includePaddingTop={false}
73+
includeSafeAreaPaddingBottom={false}
74+
testID={CountrySelectorModal.displayName}
75+
>
76+
<HeaderWithBackButton
77+
title={label}
78+
shouldShowBackButton
79+
onBackButtonPress={onClose}
80+
/>
81+
<SelectionList
82+
headerMessage={headerMessage}
83+
sections={[{data: searchResults}]}
84+
textInputValue={searchValue}
85+
textInputLabel={translate('common.search')}
86+
onChangeText={setSearchValue}
87+
onSelectRow={onCountrySelected}
88+
ListItem={RadioListItem}
89+
initiallyFocusedOptionKey={currentCountry}
90+
shouldSingleExecuteRowSelect
91+
shouldStopPropagation
92+
shouldUseDynamicMaxToRenderPerBatch
93+
/>
94+
</ScreenWrapper>
95+
</Modal>
96+
);
97+
}
98+
99+
CountrySelectorModal.displayName = 'CountrySelectorModal';
100+
101+
export default CountrySelectorModal;
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, {useState} from 'react';
2+
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
3+
import useLocalize from '@hooks/useLocalize';
4+
import Navigation from '@libs/Navigation/Navigation';
5+
import type {CountryData} from '@libs/searchCountryOptions';
6+
import CONST from '@src/CONST';
7+
import type {TranslationPaths} from '@src/languages/types';
8+
import CountrySelectorModal from './CountrySelectorModal';
9+
10+
type CountryPickerProps = {
11+
/** Current value of the selected item */
12+
value?: string;
13+
14+
/** Callback when the list item is selected */
15+
onInputChange?: (value: string, key?: string) => void;
16+
17+
/** Form Error description */
18+
errorText?: string;
19+
};
20+
21+
function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPickerProps) {
22+
const {translate} = useLocalize();
23+
const [isPickerVisible, setIsPickerVisible] = useState(false);
24+
25+
const hidePickerModal = () => {
26+
setIsPickerVisible(false);
27+
};
28+
29+
const updateInput = (item: CountryData) => {
30+
onInputChange?.(item.value);
31+
hidePickerModal();
32+
};
33+
34+
return (
35+
<>
36+
<MenuItemWithTopDescription
37+
shouldShowRightIcon
38+
title={value ? translate(`allCountries.${value}` as TranslationPaths) : undefined}
39+
description={translate('common.country')}
40+
onPress={() => setIsPickerVisible(true)}
41+
brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
42+
errorText={errorText}
43+
/>
44+
<CountrySelectorModal
45+
isVisible={isPickerVisible}
46+
currentCountry={value ?? ''}
47+
onCountrySelected={updateInput}
48+
onClose={hidePickerModal}
49+
label={translate('common.country')}
50+
onBackdropPress={Navigation.dismissModal}
51+
/>
52+
</>
53+
);
54+
}
55+
56+
CountryPicker.displayName = 'CountryPicker';
57+
export default CountryPicker;

src/components/Form/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type AmountForm from '@components/AmountForm';
77
import type AmountPicker from '@components/AmountPicker';
88
import type AmountTextInput from '@components/AmountTextInput';
99
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
10+
import type CountryPicker from '@components/CountryPicker';
1011
import type CountrySelector from '@components/CountrySelector';
1112
import type CurrencySelector from '@components/CurrencySelector';
1213
import type DatePicker from '@components/DatePicker';
@@ -16,6 +17,7 @@ import type Picker from '@components/Picker';
1617
import type RadioButtons from '@components/RadioButtons';
1718
import type RoomNameInput from '@components/RoomNameInput';
1819
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
20+
import type StatePicker from '@components/StatePicker';
1921
import type StateSelector from '@components/StateSelector';
2022
import type TextInput from '@components/TextInput';
2123
import type TextPicker from '@components/TextPicker';
@@ -57,7 +59,9 @@ type ValidInputs =
5759
| typeof EmojiPickerButtonDropdown
5860
| typeof NetSuiteCustomListPicker
5961
| typeof NetSuiteCustomFieldMappingPicker
60-
| typeof NetSuiteMenuWithTopDescriptionForm;
62+
| typeof NetSuiteMenuWithTopDescriptionForm
63+
| typeof CountryPicker
64+
| typeof StatePicker;
6165

6266
type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
6367
type ValueTypeMap = {

src/components/ReportActionItem/IssueCardMessage.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,37 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
1616

1717
type IssueCardMessageProps = {
1818
action: OnyxEntry<ReportAction>;
19+
20+
policyID: string | undefined;
1921
};
2022

2123
type IssueNewCardOriginalMessage = OriginalMessage<
2224
typeof CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS | typeof CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED | typeof CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL
2325
>;
2426

25-
function IssueCardMessage({action}: IssueCardMessageProps) {
27+
function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
2628
const {translate} = useLocalize();
2729
const styles = useThemeStyles();
2830
const {environmentURL} = useEnvironment();
2931
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
32+
const [session] = useOnyx(ONYXKEYS.SESSION);
33+
34+
const assigneeAccountID = (action?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID;
3035

31-
const assignee = `<mention-user accountID=${(action?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID}></mention-user>`;
36+
const assignee = `<mention-user accountID=${assigneeAccountID}></mention-user>`;
3237
const link = `<a href='${environmentURL}/${ROUTES.SETTINGS_WALLET}'>${translate('cardPage.expensifyCard')}</a>`;
3338

34-
const noMailingAddress = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && isEmptyObject(privatePersonalDetails?.address);
39+
const missingDetails =
40+
!privatePersonalDetails?.legalFirstName ||
41+
!privatePersonalDetails?.legalLastName ||
42+
!privatePersonalDetails?.dob ||
43+
!privatePersonalDetails?.phoneNumber ||
44+
isEmptyObject(privatePersonalDetails?.addresses) ||
45+
privatePersonalDetails.addresses.length === 0;
46+
47+
const isAssigneeCurrentUser = !isEmptyObject(session) && session.accountID === assigneeAccountID;
48+
49+
const shouldShowDetailsButton = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && missingDetails && isAssigneeCurrentUser;
3550

3651
const getTranslation = () => {
3752
switch (action?.actionName) {
@@ -40,7 +55,7 @@ function IssueCardMessage({action}: IssueCardMessageProps) {
4055
case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL:
4156
return translate('workspace.expensifyCard.issuedCardVirtual', {assignee, link});
4257
case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS:
43-
return translate(`workspace.expensifyCard.${noMailingAddress ? 'issuedCardNoMailingAddress' : 'addedAddress'}`, assignee);
58+
return translate(`workspace.expensifyCard.${!isAssigneeCurrentUser || shouldShowDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, assignee);
4459
default:
4560
return '';
4661
}
@@ -49,13 +64,18 @@ function IssueCardMessage({action}: IssueCardMessageProps) {
4964
return (
5065
<>
5166
<RenderHTML html={`<muted-text>${getTranslation()}</muted-text>`} />
52-
{noMailingAddress && (
67+
{shouldShowDetailsButton && (
5368
<Button
54-
onPress={() => Navigation.navigate(ROUTES.SETTINGS_ADDRESS)}
69+
onPress={() => {
70+
if (!policyID) {
71+
return;
72+
}
73+
Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS.getRoute(policyID));
74+
}}
5575
success
5676
medium
5777
style={[styles.alignSelfStart, styles.mt3]}
58-
text={translate('workspace.expensifyCard.addMailingAddress')}
78+
text={translate('workspace.expensifyCard.addShippingDetails')}
5979
/>
6080
)}
6181
</>

0 commit comments

Comments
 (0)