diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f4dd60d81df34..7db14bccd572f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2892,6 +2892,7 @@ const ROUTES = { getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const, }, MISSING_PERSONAL_DETAILS: 'missing-personal-details', + MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'missing-personal-details/confirm-magic-code', POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: { route: 'workspaces/:policyID/accounting/netsuite/subsidiary-selector', getRoute: (policyID: string | undefined) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ea8070f3ad88c..eb5fd718bdc49 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -801,6 +801,7 @@ const SCREENS = { FEATURE_TRAINING_ROOT: 'FeatureTraining_Root', RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root', MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root', + MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'MissingPersonalDetails_ConfirmMagicCode', ADD_UNREPORTED_EXPENSES_ROOT: 'AddUnreportedExpenses_Root', DEBUG: { REPORT: 'Debug_Report', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 3b74d0ea26fdf..ac9f319e93384 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -909,6 +909,7 @@ const ShareModalStackNavigator = createModalStackNavigator({ [SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: () => require('../../../../pages/MissingPersonalDetails').default, + [SCREENS.MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: () => require('../../../../pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodePage').default, }); const AddUnreportedExpenseModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index d898dcd7fada0..b689585fdf40b 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1628,6 +1628,7 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.MISSING_PERSONAL_DETAILS]: { screens: { [SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: ROUTES.MISSING_PERSONAL_DETAILS, + [SCREENS.MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: ROUTES.MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE, }, }, [SCREENS.RIGHT_MODAL.ADD_UNREPORTED_EXPENSE]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ff407434ee6e1..cd37adf4b5564 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2632,6 +2632,7 @@ type RestrictedActionParamList = { type MissingPersonalDetailsParamList = { [SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: undefined; + [SCREENS.MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: undefined; }; type SplitExpenseParamList = { diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 328ef1798e2cc..1eced4d84cdb6 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -421,6 +421,20 @@ const getPhoneNumber = (details: OnyxEntry): string | undefined return login ? Str.removeSMSDomain(login) : ''; }; +/** + * Checks whether any personal details are missing + */ +function arePersonalDetailsMissing(privatePersonalDetails: OnyxEntry): boolean { + return ( + !privatePersonalDetails?.legalFirstName || + !privatePersonalDetails?.legalLastName || + !privatePersonalDetails?.dob || + !privatePersonalDetails?.phoneNumber || + isEmptyObject(privatePersonalDetails?.addresses) || + privatePersonalDetails.addresses.length === 0 + ); +} + export { getDisplayNameOrDefault, getPersonalDetailsByIDs, @@ -440,4 +454,5 @@ export { getShortMentionIfFound, getLoginByAccountID, getPhoneNumber, + arePersonalDetailsMissing, }; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ffb8297edc508..0d5d7abddfb8d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -32,7 +32,7 @@ import {formatMessageElementList, translateLocal} from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; import Parser from './Parser'; -import {getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; +import {arePersonalDetailsMissing, getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils'; import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; @@ -3115,13 +3115,7 @@ function isCardIssuedAction( } function shouldShowAddMissingDetails(actionName?: ReportActionName, card?: Card) { - const missingDetails = - !privatePersonalDetails?.legalFirstName || - !privatePersonalDetails?.legalLastName || - !privatePersonalDetails?.dob || - !privatePersonalDetails?.phoneNumber || - isEmptyObject(privatePersonalDetails?.addresses) || - privatePersonalDetails.addresses.length === 0; + const missingDetails = arePersonalDetailsMissing(privatePersonalDetails); return actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && (card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED || missingDetails); } diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx index 26abb79a033f0..9071ad5ee7a1c 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx @@ -7,15 +7,14 @@ import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useSubStep from '@hooks/useSubStep'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDraftValues} from '@libs/actions/FormActions'; -import {updatePersonalDetailsAndShipExpensifyCards} from '@libs/actions/PersonalDetails'; import {normalizeCountryCode} from '@libs/CountryUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {PersonalDetailsForm} from '@src/types/form'; import type {PrivatePersonalDetails} from '@src/types/onyx'; import MissingPersonalDetailsMagicCodeModal from './MissingPersonalDetailsMagicCodeModal'; @@ -44,7 +43,6 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, hea const styles = useThemeStyles(); const {translate} = useLocalize(); const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); - const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const ref: ForwardedRef = useRef(null); @@ -56,8 +54,12 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, hea if (!values) { return; } + if (!onComplete) { + Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE); + return; + } setIsValidateCodeActionModalVisible(true); - }, [values]); + }, [onComplete, values]); const { componentToRender: SubStep, @@ -90,13 +92,12 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, hea const handleSubmitForm = useCallback( (validateCode: string) => { - if (onComplete) { - onComplete(values, validateCode); + if (!onComplete) { return; } - updatePersonalDetailsAndShipExpensifyCards(values, validateCode, countryCode); + onComplete(values, validateCode); }, - [countryCode, values, onComplete], + [values, onComplete], ); const handleNextScreen = useCallback(() => { @@ -122,7 +123,6 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, hea includeSafeAreaPaddingBottom={false} shouldEnableMaxHeight testID={MissingPersonalDetailsContent.displayName} - shouldShowOfflineIndicatorInWideScreen={!!isValidateCodeActionModalVisible} > - setIsValidateCodeActionModalVisible(false)} isValidateCodeActionModalVisible={isValidateCodeActionModalVisible} diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx index 8a32c37ce4b42..cb06e651b2432 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodeModal.tsx @@ -7,6 +7,7 @@ import {clearPersonalDetailsErrors} from '@libs/actions/PersonalDetails'; import {requestValidateCodeAction} from '@libs/actions/User'; import {getLatestError} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import {arePersonalDetailsMissing} from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -28,13 +29,7 @@ function MissingPersonalDetailsMagicCodeModal({onClose, isValidateCodeActionModa const primaryLogin = account?.primaryLogin ?? ''; const areAllCardsShipped = Object.values(cardList ?? {})?.every((card) => card?.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const missingDetails = - !privatePersonalDetails?.legalFirstName || - !privatePersonalDetails?.legalLastName || - !privatePersonalDetails?.dob || - !privatePersonalDetails?.phoneNumber || - isEmptyObject(privatePersonalDetails?.addresses) || - privatePersonalDetails.addresses.length === 0; + const missingDetails = arePersonalDetailsMissing(privatePersonalDetails); useEffect(() => { if (missingDetails || !!privateDetailsErrors || !areAllCardsShipped) { diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodePage.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodePage.tsx new file mode 100644 index 0000000000000..965035ab40551 --- /dev/null +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsMagicCodePage.tsx @@ -0,0 +1,84 @@ +import React, {useCallback, useEffect, useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import {clearPersonalDetailsErrors, updatePersonalDetailsAndShipExpensifyCards} from '@libs/actions/PersonalDetails'; +import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; +import {normalizeCountryCode} from '@libs/CountryUtils'; +import {getLatestError} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {arePersonalDetailsMissing} from '@libs/PersonalDetailsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {primaryLoginSelector} from '@src/selectors/Account'; +import type {PersonalDetailsForm} from '@src/types/form'; +import type {CardList} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getSubstepValues} from './utils'; + +const areAllCardsShippedSelector = (cardList: OnyxEntry) => Object.values(cardList ?? {})?.every((card) => card?.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); + +function MissingPersonalDetailsMagicCodePage() { + const {translate} = useLocalize(); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: false}); + const [draftValues] = useOnyx(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM_DRAFT, {canBeMissing: false}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + + const [areAllCardsShipped] = useOnyx(ONYXKEYS.CARD_LIST, {selector: areAllCardsShippedSelector, canBeMissing: true}); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: primaryLoginSelector, canBeMissing: true}); + + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); + const privateDetailsErrors = privatePersonalDetails?.errors ?? undefined; + const validateLoginError = getLatestError(privateDetailsErrors); + + const missingDetails = arePersonalDetailsMissing(privatePersonalDetails); + + useEffect(() => { + if (missingDetails || !!privateDetailsErrors || !areAllCardsShipped) { + return; + } + + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); + Navigation.dismissModal(); + }, [missingDetails, privateDetailsErrors, areAllCardsShipped]); + + const clearError = () => { + if (isEmptyObject(validateLoginError) && isEmptyObject(validateCodeAction?.errorFields)) { + return; + } + clearPersonalDetailsErrors(); + }; + + const values = useMemo(() => normalizeCountryCode(getSubstepValues(privatePersonalDetails, draftValues)) as PersonalDetailsForm, [privatePersonalDetails, draftValues]); + + const handleSubmitForm = useCallback( + (validateCode: string) => { + updatePersonalDetailsAndShipExpensifyCards(values, validateCode, countryCode); + }, + [countryCode, values], + ); + + return ( + requestValidateCodeAction()} + validateCodeActionErrorField="personalDetails" + handleSubmitForm={handleSubmitForm} + validateError={validateLoginError} + clearError={clearError} + onClose={() => { + resetValidateActionCodeSent(); + Navigation.goBack(ROUTES.MISSING_PERSONAL_DETAILS); + }} + isLoading={privatePersonalDetails?.isLoading} + /> + ); +} + +MissingPersonalDetailsMagicCodePage.displayName = 'MissingPersonalDetailsMagicCodePage'; + +export default MissingPersonalDetailsMagicCodePage; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index 09cde28410da2..f4dfeebb22489 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -27,6 +27,7 @@ import {convertToDisplayString, getCurrencyKeyByCountryCode} from '@libs/Currenc import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DomainCardNavigatorParamList, SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {arePersonalDetailsMissing} from '@libs/PersonalDetailsUtils'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import RedDotCardSection from '@pages/settings/Wallet/RedDotCardSection'; @@ -59,13 +60,7 @@ type LimitTypeTranslationKeys = { */ function shouldShowMissingDetailsPage(card: OnyxEntry, privatePersonalDetails: OnyxEntry): boolean { const isUKOrEUCard = card?.nameValuePairs?.feedCountry === 'GB'; - const hasMissingDetails = - !privatePersonalDetails?.legalFirstName || - !privatePersonalDetails?.legalLastName || - !privatePersonalDetails?.dob || - !privatePersonalDetails?.phoneNumber || - !privatePersonalDetails?.addresses || - privatePersonalDetails.addresses.length === 0; + const hasMissingDetails = arePersonalDetailsMissing(privatePersonalDetails); return hasMissingDetails && isUKOrEUCard; } diff --git a/src/selectors/Account.ts b/src/selectors/Account.ts index 28b3333d6ad49..ed688df9a4739 100644 --- a/src/selectors/Account.ts +++ b/src/selectors/Account.ts @@ -5,4 +5,6 @@ const isActingAsDelegateSelector = (account: OnyxEntry) => !!account?.d const isUserValidatedSelector = (account: OnyxEntry) => account?.validated; -export {isActingAsDelegateSelector, isUserValidatedSelector}; +const primaryLoginSelector = (account: OnyxEntry) => account?.primaryLogin; + +export {isActingAsDelegateSelector, isUserValidatedSelector, primaryLoginSelector}; diff --git a/src/selectors/PersonalDetails.ts b/src/selectors/PersonalDetails.ts index 8a787a47962c8..a11d3ff3dea6c 100644 --- a/src/selectors/PersonalDetails.ts +++ b/src/selectors/PersonalDetails.ts @@ -11,5 +11,4 @@ const createPersonalDetailsSelector = (personalDetails: OnyxEntry) => personalDetails ? lodashMapKeys(personalDetails, (value, key) => value?.login ?? key) : undefined; -// eslint-disable-next-line import/prefer-default-export export {createPersonalDetailsSelector, personalDetailsByEmailSelector};