Skip to content

Commit

Permalink
Merge pull request #48775 from shubham1206agra/missing-personal-details
Browse files Browse the repository at this point in the history
  • Loading branch information
mountiny authored Sep 12, 2024
2 parents 5327409 + 72b265f commit 336e478
Show file tree
Hide file tree
Showing 30 changed files with 1,044 additions and 16 deletions.
10 changes: 10 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1825,6 +1825,16 @@ const CONST = {
VENDOR_BILL: 'bill',
},

MISSING_PERSONAL_DETAILS_INDEXES: {
MAPPING: {
LEGAL_NAME: 0,
DATE_OF_BIRTH: 1,
ADDRESS: 2,
PHONE_NUMBER: 3,
},
INDEX_LIST: ['1', '2', '3', '4'],
},

ACCOUNT_ID: {
ACCOUNTING: Number(Config?.EXPENSIFY_ACCOUNT_ID_ACCOUNTING ?? 9645353),
ADMIN: Number(Config?.EXPENSIFY_ACCOUNT_ID_ADMIN ?? -1),
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@ const ONYXKEYS = {
DATE_OF_BIRTH_FORM_DRAFT: 'dateOfBirthFormDraft',
HOME_ADDRESS_FORM: 'homeAddressForm',
HOME_ADDRESS_FORM_DRAFT: 'homeAddressFormDraft',
PERSONAL_DETAILS_FORM: 'personalDetailsForm',
PERSONAL_DETAILS_FORM_DRAFT: 'personalDetailsFormDraft',
NEW_ROOM_FORM: 'newRoomForm',
NEW_ROOM_FORM_DRAFT: 'newRoomFormDraft',
ROOM_SETTINGS_FORM: 'roomSettingsForm',
Expand Down Expand Up @@ -704,6 +706,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm;
[ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: FormTypes.DateOfBirthForm;
[ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.HomeAddressForm;
[ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM]: FormTypes.PersonalDetailsForm;
[ONYXKEYS.FORMS.NEW_ROOM_FORM]: FormTypes.NewRoomForm;
[ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm;
[ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,10 @@ const ROUTES = {
route: 'restricted-action/workspace/:policyID',
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
},
MISSING_PERSONAL_DETAILS: {
route: 'missing-personal-details/workspace/:policyID',
getRoute: (policyID: string) => `missing-personal-details/workspace/${policyID}` as const,
},
POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
route: 'settings/workspaces/:policyID/accounting/netsuite/subsidiary-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/subsidiary-selector` as const,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const SCREENS = {
SETTINGS_CATEGORIES: 'SettingsCategories',
RESTRICTED_ACTION: 'RestrictedAction',
REPORT_EXPORT: 'Report_Export',
MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails',
},
ONBOARDING_MODAL: {
ONBOARDING: 'Onboarding',
Expand Down Expand Up @@ -542,6 +543,7 @@ const SCREENS = {
TRANSACTION_RECEIPT: 'TransactionReceipt',
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root',
} as const;

type Screen = DeepValueOf<typeof SCREENS>;
Expand Down
101 changes: 101 additions & 0 deletions src/components/CountryPicker/CountrySelectorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, {useMemo} from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import searchCountryOptions from '@libs/searchCountryOptions';
import type {CountryData} from '@libs/searchCountryOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';

type CountrySelectorModalProps = {
/** Whether the modal is visible */
isVisible: boolean;

/** Function to call when the user closes the business type selector modal */
onClose: () => void;

/** Label to display on field */
label: string;

/** Country selected */
currentCountry: string;

/** Function to call when the user selects a country */
onCountrySelected: (value: CountryData) => void;

/** Function to call when the user presses on the modal backdrop */
onBackdropPress?: () => void;
};

function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onClose, label, onBackdropPress}: CountrySelectorModalProps) {
const {translate} = useLocalize();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');

const countries = useMemo(
() =>
Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => {
const countryName = translate(`allCountries.${countryISO}` as TranslationPaths);
return {
value: countryISO,
keyForList: countryISO,
text: countryName,
isSelected: currentCountry === countryISO,
searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
};
}),
[translate, currentCountry],
);

const searchResults = searchCountryOptions(debouncedSearchValue, countries);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

const styles = useThemeStyles();

return (
<Modal
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
isVisible={isVisible}
onClose={onClose}
onModalHide={onClose}
hideModalContentWhileAnimating
useNativeDriver
onBackdropPress={onBackdropPress}
>
<ScreenWrapper
style={[styles.pb0]}
includePaddingTop={false}
includeSafeAreaPaddingBottom={false}
testID={CountrySelectorModal.displayName}
>
<HeaderWithBackButton
title={label}
shouldShowBackButton
onBackButtonPress={onClose}
/>
<SelectionList
headerMessage={headerMessage}
sections={[{data: searchResults}]}
textInputValue={searchValue}
textInputLabel={translate('common.search')}
onChangeText={setSearchValue}
onSelectRow={onCountrySelected}
ListItem={RadioListItem}
initiallyFocusedOptionKey={currentCountry}
shouldSingleExecuteRowSelect
shouldStopPropagation
shouldUseDynamicMaxToRenderPerBatch
/>
</ScreenWrapper>
</Modal>
);
}

CountrySelectorModal.displayName = 'CountrySelectorModal';

export default CountrySelectorModal;
57 changes: 57 additions & 0 deletions src/components/CountryPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, {useState} from 'react';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {CountryData} from '@libs/searchCountryOptions';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import CountrySelectorModal from './CountrySelectorModal';

type CountryPickerProps = {
/** Current value of the selected item */
value?: string;

/** Callback when the list item is selected */
onInputChange?: (value: string, key?: string) => void;

/** Form Error description */
errorText?: string;
};

function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPickerProps) {
const {translate} = useLocalize();
const [isPickerVisible, setIsPickerVisible] = useState(false);

const hidePickerModal = () => {
setIsPickerVisible(false);
};

const updateInput = (item: CountryData) => {
onInputChange?.(item.value);
hidePickerModal();
};

return (
<>
<MenuItemWithTopDescription
shouldShowRightIcon
title={value ? translate(`allCountries.${value}` as TranslationPaths) : undefined}
description={translate('common.country')}
onPress={() => setIsPickerVisible(true)}
brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={errorText}
/>
<CountrySelectorModal
isVisible={isPickerVisible}
currentCountry={value ?? ''}
onCountrySelected={updateInput}
onClose={hidePickerModal}
label={translate('common.country')}
onBackdropPress={Navigation.dismissModal}
/>
</>
);
}

CountryPicker.displayName = 'CountryPicker';
export default CountryPicker;
6 changes: 5 additions & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type AmountForm from '@components/AmountForm';
import type AmountPicker from '@components/AmountPicker';
import type AmountTextInput from '@components/AmountTextInput';
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
import type CountryPicker from '@components/CountryPicker';
import type CountrySelector from '@components/CountrySelector';
import type CurrencySelector from '@components/CurrencySelector';
import type DatePicker from '@components/DatePicker';
Expand All @@ -16,6 +17,7 @@ import type Picker from '@components/Picker';
import type RadioButtons from '@components/RadioButtons';
import type RoomNameInput from '@components/RoomNameInput';
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
import type StatePicker from '@components/StatePicker';
import type StateSelector from '@components/StateSelector';
import type TextInput from '@components/TextInput';
import type TextPicker from '@components/TextPicker';
Expand Down Expand Up @@ -57,7 +59,9 @@ type ValidInputs =
| typeof EmojiPickerButtonDropdown
| typeof NetSuiteCustomListPicker
| typeof NetSuiteCustomFieldMappingPicker
| typeof NetSuiteMenuWithTopDescriptionForm;
| typeof NetSuiteMenuWithTopDescriptionForm
| typeof CountryPicker
| typeof StatePicker;

type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
type ValueTypeMap = {
Expand Down
34 changes: 27 additions & 7 deletions src/components/ReportActionItem/IssueCardMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,37 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';

type IssueCardMessageProps = {
action: OnyxEntry<ReportAction>;

policyID: string | undefined;
};

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

function IssueCardMessage({action}: IssueCardMessageProps) {
function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {environmentURL} = useEnvironment();
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
const [session] = useOnyx(ONYXKEYS.SESSION);

const assigneeAccountID = (action?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID;

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

const noMailingAddress = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && isEmptyObject(privatePersonalDetails?.address);
const missingDetails =
!privatePersonalDetails?.legalFirstName ||
!privatePersonalDetails?.legalLastName ||
!privatePersonalDetails?.dob ||
!privatePersonalDetails?.phoneNumber ||
isEmptyObject(privatePersonalDetails?.addresses) ||
privatePersonalDetails.addresses.length === 0;

const isAssigneeCurrentUser = !isEmptyObject(session) && session.accountID === assigneeAccountID;

const shouldShowDetailsButton = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && missingDetails && isAssigneeCurrentUser;

const getTranslation = () => {
switch (action?.actionName) {
Expand All @@ -40,7 +55,7 @@ function IssueCardMessage({action}: IssueCardMessageProps) {
case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL:
return translate('workspace.expensifyCard.issuedCardVirtual', {assignee, link});
case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS:
return translate(`workspace.expensifyCard.${noMailingAddress ? 'issuedCardNoMailingAddress' : 'addedAddress'}`, assignee);
return translate(`workspace.expensifyCard.${!isAssigneeCurrentUser || shouldShowDetailsButton ? 'issuedCardNoShippingDetails' : 'addedShippingDetails'}`, assignee);
default:
return '';
}
Expand All @@ -49,13 +64,18 @@ function IssueCardMessage({action}: IssueCardMessageProps) {
return (
<>
<RenderHTML html={`<muted-text>${getTranslation()}</muted-text>`} />
{noMailingAddress && (
{shouldShowDetailsButton && (
<Button
onPress={() => Navigation.navigate(ROUTES.SETTINGS_ADDRESS)}
onPress={() => {
if (!policyID) {
return;
}
Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS.getRoute(policyID));
}}
success
medium
style={[styles.alignSelfStart, styles.mt3]}
text={translate('workspace.expensifyCard.addMailingAddress')}
text={translate('workspace.expensifyCard.addShippingDetails')}
/>
)}
</>
Expand Down
Loading

0 comments on commit 336e478

Please sign in to comment.