From b8ae247fa750295c7212af7b4c4f311de9ca4ae3 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jul 2024 14:09:07 +0200 Subject: [PATCH 1/7] Create IOURequestStepCompanyInfo page --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 5 ++ src/SCREENS.ts | 1 + src/languages/en.ts | 5 ++ src/languages/es.ts | 5 ++ .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 6 ++ .../step/IOURequestStepCompanyInfo.tsx | 76 +++++++++++++++++++ .../step/withFullTransactionOrNotFound.tsx | 3 +- .../step/withWritableReportOrNotFound.tsx | 3 +- src/types/form/MoneyRequestCompanyInfoForm.ts | 20 +++++ src/types/form/index.ts | 1 + 13 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx create mode 100644 src/types/form/MoneyRequestCompanyInfoForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index dfbe28fe520..ccb4f510ebf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -523,6 +523,8 @@ const ONYXKEYS = { MONEY_REQUEST_DATE_FORM_DRAFT: 'moneyRequestCreatedFormDraft', MONEY_REQUEST_HOLD_FORM: 'moneyHoldReasonForm', MONEY_REQUEST_HOLD_FORM_DRAFT: 'moneyHoldReasonFormDraft', + MONEY_REQUEST_COMPANY_INFO_FORM: 'moneyRequestCompanyInfoForm', + MONEY_REQUEST_COMPANY_INFO_FORM_DRAFT: 'moneyRequestCompanyInfoFormDraft', NEW_CONTACT_METHOD_FORM: 'newContactMethodForm', NEW_CONTACT_METHOD_FORM_DRAFT: 'newContactMethodFormDraft', WAYPOINT_FORM: 'waypointForm', @@ -628,6 +630,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.MoneyRequestAmountForm; [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.MoneyRequestDateForm; [ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM]: FormTypes.MoneyRequestHoldReasonForm; + [ONYXKEYS.FORMS.MONEY_REQUEST_COMPANY_INFO_FORM]: FormTypes.MoneyRequestCompanyInfoForm; [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.NewContactMethodForm; [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f4e78005ccb..96e5fd6c384 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -363,6 +363,11 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType as string}/from/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_COMPANY_INFO: { + route: 'create/:iouType/company-info/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType as string}/company-info/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 15361ee049a..53cd433206b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -183,6 +183,7 @@ const SCREENS = { STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer', STEP_SEND_FROM: 'Money_Request_Step_Send_From', + STEP_COMPANY_INFO: 'Money_Request_Step_Company_Info', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', diff --git a/src/languages/en.ts b/src/languages/en.ts index 0275c90d551..9c89c7cbb9f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -710,6 +710,11 @@ export default { receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", receiptScanningFailed: 'Receipt scanning failed. Please enter the details manually.', transactionPendingDescription: 'Transaction pending. It may take a few days to post.', + companyInfo: 'Company info', + companyInfoDescription: 'We need a few more details before you can send your first invoice.', + yourCompanyName: 'Your company name', + yourCompanyWebsite: 'Your company website', + yourCompanyWebsiteNote: "If you don't have a website, you can provide your company's LinkedIn or social media profile instead.", expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' diff --git a/src/languages/es.ts b/src/languages/es.ts index 8a9c5f1e81d..b9b715d4ba8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -705,6 +705,11 @@ export default { receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.', transactionPendingDescription: 'Transacción pendiente. Puede tardar unos días en contabilizarse.', + companyInfo: 'Información de la empresa', + companyInfoDescription: 'Necesitamos algunos detalles más antes de que pueda enviar su primera factura.', + yourCompanyName: 'Nombre de su empresa', + yourCompanyWebsite: 'Sitio web de su empresa', + yourCompanyWebsiteNote: 'Si no tiene un sitio web, puede proporcionar el perfil de LinkedIn o de las redes sociales de su empresa.', expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b19dd76d9e9..66965c234d1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -90,6 +90,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default, [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: () => require('../../../../pages/iou/request/step/IOURequestStepSplitPayer').default, [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: () => require('../../../../pages/iou/request/step/IOURequestStepSendFrom').default, + [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: () => require('../../../../pages/iou/request/step/IOURequestStepCompanyInfo').default, [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../../pages/iou/HoldReasonPage').default, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default, [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 8aaf716c6e5..d92fe093b4f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -863,6 +863,7 @@ const config: LinkingOptions['config'] = { }, [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route, [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route, + [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 55e1f6203f8..feb1b831868 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -774,6 +774,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backTo: Routes; }; + [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: { + iouType: IOUType; + transactionID: string; + reportID: string; + backTo: Routes; + }; [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: { action: IOUAction; iouType: Exclude; diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx new file mode 100644 index 00000000000..1c994996c39 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import Navigation from '@navigation/Navigation'; +import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; +import withFullTransactionOrNotFound, {type WithFullTransactionOrNotFoundProps} from '@pages/iou/request/step/withFullTransactionOrNotFound'; +import withWritableReportOrNotFound, {type WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/MoneyRequestCompanyInfoForm'; + +type IOURequestStepCompanyInfoProps = WithWritableReportOrNotFoundProps & + WithFullTransactionOrNotFoundProps; + +function IOURequestStepCompanyInfo({route, transaction}: IOURequestStepCompanyInfoProps) { + const {backTo} = route.params; + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + + const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(transaction?.amount ?? 0), transaction?.currency); + + return ( + Navigation.goBack(backTo)} + shouldShowWrapper + testID={IOURequestStepCompanyInfo.displayName} + > + {translate('iou.companyInfoDescription')} + {}} + validate={() => { + return {}; + }} + submitButtonText={translate('iou.sendInvoice', {amount: formattedAmount})} + enabledWhenOffline + > + + + + + ); +} + +IOURequestStepCompanyInfo.displayName = 'IOURequestStepCompanyInfo'; + +export default withWritableReportOrNotFound(withFullTransactionOrNotFound(IOURequestStepCompanyInfo)); diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index e6072af08b7..9bf454b2c27 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -35,7 +35,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE | typeof SCREENS.MONEY_REQUEST.STEP_SCAN | typeof SCREENS.MONEY_REQUEST.STEP_CURRENCY - | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM + | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO; type Route = RouteProp; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 9539ee1e159..65ba0117fdf 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -42,7 +42,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT | typeof SCREENS.MONEY_REQUEST.STEP_SCAN - | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM + | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO; type Route = RouteProp; diff --git a/src/types/form/MoneyRequestCompanyInfoForm.ts b/src/types/form/MoneyRequestCompanyInfoForm.ts new file mode 100644 index 00000000000..94ff6867e75 --- /dev/null +++ b/src/types/form/MoneyRequestCompanyInfoForm.ts @@ -0,0 +1,20 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + COMPANY_NAME: 'companyName', + COMPANY_WEBSITE: 'companyWebsite', +} as const; + +type InputID = ValueOf; + +type MoneyRequestCompanyInfoForm = Form< + InputID, + { + [INPUT_IDS.COMPANY_NAME]: string; + [INPUT_IDS.COMPANY_WEBSITE]: string; + } +>; + +export type {MoneyRequestCompanyInfoForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 3c6946dd97e..c6ef6023d8d 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -17,6 +17,7 @@ export type {MoneyRequestDateForm} from './MoneyRequestDateForm'; export type {MoneyRequestDescriptionForm} from './MoneyRequestDescriptionForm'; export type {MoneyRequestMerchantForm} from './MoneyRequestMerchantForm'; export type {MoneyRequestHoldReasonForm} from './MoneyRequestHoldReasonForm'; +export type {MoneyRequestCompanyInfoForm} from './MoneyRequestCompanyInfoForm'; export type {NewContactMethodForm} from './NewContactMethodForm'; export type {ChangeBillingCurrencyForm} from './ChangeBillingCurrencyForm'; export type {NewRoomForm} from './NewRoomForm'; From 3005b89be3a56b7afde113b0b4b4c69586647270 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jul 2024 14:57:24 +0200 Subject: [PATCH 2/7] Add a check for invoicing details --- src/components/MoneyRequestConfirmationList.tsx | 12 +++++++++++- src/libs/actions/Policy/Policy.ts | 8 ++++++++ .../request/step/IOURequestStepCompanyInfo.tsx | 15 +++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ae8cbe4298f..ac79278e9c9 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -25,6 +25,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; +import {hasInvoicingDetails} from '@userActions/Policy/Policy'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -364,7 +365,11 @@ function MoneyRequestConfirmationList({ const splitOrRequestOptions: Array> = useMemo(() => { let text; if (isTypeInvoice) { - text = translate('iou.sendInvoice', {amount: formattedAmount}); + if (hasInvoicingDetails(policy)) { + text = translate('iou.sendInvoice', {amount: formattedAmount}); + } else { + text = translate('common.next'); + } } else if (isTypeTrackExpense) { text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { @@ -669,6 +674,11 @@ function MoneyRequestConfirmationList({ */ const confirm = useCallback( (paymentMethod: PaymentMethodType | undefined) => { + if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.getRoute(iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + return; + } + if (selectedParticipants.length === 0) { return; } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 53f33c4fd3d..8b2c4b4255e 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -203,6 +203,13 @@ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefine return primaryPolicy ?? activeAdminWorkspaces[0]; } +/** Check if the policy has invoicing company details */ +function hasInvoicingDetails(policy: OnyxEntry): boolean { + // TODO: uncomment when invoicing details inside a policy are supported. + // return !!policy.invoice.companyName && !!policy.invoice.companyWebsite; + return true; +} + /** * Check if the user has any active free policies (aka workspaces) */ @@ -3225,6 +3232,7 @@ export { requestExpensifyCardLimitIncrease, getAdminPoliciesConnectedToNetSuite, getAdminPoliciesConnectedToSageIntacct, + hasInvoicingDetails, }; export type {NewCustomUnit}; diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index 1c994996c39..be3d17e0c85 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -7,14 +7,17 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; import Navigation from '@navigation/Navigation'; -import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; -import withFullTransactionOrNotFound, {type WithFullTransactionOrNotFoundProps} from '@pages/iou/request/step/withFullTransactionOrNotFound'; -import withWritableReportOrNotFound, {type WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestCompanyInfoForm'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; type IOURequestStepCompanyInfoProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; @@ -39,10 +42,10 @@ function IOURequestStepCompanyInfo({route, transaction}: IOURequestStepCompanyIn {}} - validate={() => { - return {}; + onSubmit={() => { + playSound(SOUNDS.DONE); }} + validate={() => ({})} submitButtonText={translate('iou.sendInvoice', {amount: formattedAmount})} enabledWhenOffline > From 5d4bc34f1924f0db8c187a51837b2cf0d5adbc26 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jul 2024 16:11:18 +0200 Subject: [PATCH 3/7] Implemented validation on company info page --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/ValidationUtils.ts | 8 +++- .../step/IOURequestStepCompanyInfo.tsx | 41 ++++++++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9c89c7cbb9f..14aaebec784 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -715,6 +715,8 @@ export default { yourCompanyName: 'Your company name', yourCompanyWebsite: 'Your company website', yourCompanyWebsiteNote: "If you don't have a website, you can provide your company's LinkedIn or social media profile instead.", + invalidDomainError: 'You have entered an invalid domain. To continue, please enter a valid domain.', + publicDomainError: 'You have entered a public domain. To continue, please enter a private domain.', expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' diff --git a/src/languages/es.ts b/src/languages/es.ts index b9b715d4ba8..28743fd861d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -710,6 +710,8 @@ export default { yourCompanyName: 'Nombre de su empresa', yourCompanyWebsite: 'Sitio web de su empresa', yourCompanyWebsiteNote: 'Si no tiene un sitio web, puede proporcionar el perfil de LinkedIn o de las redes sociales de su empresa.', + invalidDomainError: 'Ha introducido un dominio no válido. Para continuar, introduzca un dominio válido.', + publicDomainError: 'Ha introducido un dominio público. Para continuar, introduzca un dominio privado.', expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 64cae69e0b1..14c5e493cb2 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -1,5 +1,5 @@ import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns'; -import {Str, Url} from 'expensify-common'; +import {PUBLIC_DOMAINS, Str, Url} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; import type {OnyxCollection} from 'react-native-onyx'; @@ -242,6 +242,11 @@ function isValidWebsite(url: string): boolean { return new RegExp(`^${Url.URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase; } +/** Checks if the domain is public */ +function isPublicDomain(domain: string): boolean { + return PUBLIC_DOMAINS.some((publicDomain) => publicDomain === domain); +} + function validateIdentity(identity: Record): Record { const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; const errors: Record = {}; @@ -534,4 +539,5 @@ export { isExistingTaxName, isValidSubscriptionSize, isExistingTaxCode, + isPublicDomain, }; diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index be3d17e0c85..ac975f10517 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -1,6 +1,8 @@ -import React from 'react'; +import {Str} from 'expensify-common'; +import React, {useCallback} from 'react'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -8,6 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import playSound, {SOUNDS} from '@libs/Sound'; +import * as ValidationUtils from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -31,6 +34,40 @@ function IOURequestStepCompanyInfo({route, transaction}: IOURequestStepCompanyIn const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(transaction?.amount ?? 0), transaction?.currency); + const extractUrlDomain = (url: string): string | undefined => { + const DOMAIN_BASE_REGEX = '^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)'; + const match = String(url).match(DOMAIN_BASE_REGEX); + + if (!match) { + return undefined; + } + + return match[1]; + }; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_NAME, INPUT_IDS.COMPANY_WEBSITE]); + + if (values.companyWebsite) { + if (!ValidationUtils.isValidWebsite(values.companyWebsite)) { + errors.companyWebsite = translate('bankAccount.error.website'); + } else { + const domain = extractUrlDomain(values.companyWebsite); + + if (!domain || !Str.isValidDomainName(domain)) { + errors.companyWebsite = translate('iou.invalidDomainError'); + } else if (ValidationUtils.isPublicDomain(domain)) { + errors.companyWebsite = translate('iou.publicDomainError'); + } + } + } + + return errors; + }, + [translate], + ); + return ( { playSound(SOUNDS.DONE); }} - validate={() => ({})} + validate={validate} submitButtonText={translate('iou.sendInvoice', {amount: formattedAmount})} enabledWhenOffline > From b5320622f4ff64ae9cb0daa47461b521ea5ea5fb Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jul 2024 12:28:04 +0200 Subject: [PATCH 4/7] Update sendInvoice function --- src/libs/API/parameters/SendInvoiceParams.ts | 2 + src/libs/actions/IOU.ts | 55 ++++++++++++++++++- .../step/IOURequestStepCompanyInfo.tsx | 19 +++++-- src/types/onyx/Policy.ts | 12 ++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index f8ba0647fb0..c95ffce14b2 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -18,6 +18,8 @@ type SendInvoiceParams = RequireAtLeastOne< reportPreviewReportActionID: string; transactionID: string; transactionThreadReportID: string; + companyName?: string; + companyWebsite?: string; }, 'receiverEmail' | 'receiverInvoiceRoomID' >; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5c908129a53..08cb27d6e0f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -898,6 +898,8 @@ function buildOnyxDataForInvoice( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, + companyName?: string, + companyWebsite?: string, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = [ @@ -1175,6 +1177,49 @@ function buildOnyxDataForInvoice( }, ]; + if (companyName && companyWebsite) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + value: { + invoice: { + companyName, + companyWebsite, + pendingFields: { + companyName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + companyWebsite: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + value: { + invoice: { + pendingFields: { + companyName: null, + companyWebsite: null, + }, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + value: { + invoice: { + companyName: undefined, + companyWebsite: undefined, + pendingFields: { + companyName: null, + companyWebsite: null, + }, + }, + }, + }); + } + // We don't need to compute violations unless we're on a paid policy if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { return [optimisticData, successData, failureData]; @@ -1771,6 +1816,8 @@ function getSendInvoiceInformation( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, + companyName?: string, + companyWebsite?: string, ): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); @@ -1878,6 +1925,8 @@ function getSendInvoiceInformation( policy, policyTagList, policyCategories, + companyName, + companyWebsite, ); return { @@ -3602,9 +3651,11 @@ function sendInvoice( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, + companyName?: string, + companyWebsite?: string, ) { const {senderWorkspaceID, receiver, invoiceRoom, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} = - getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); + getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories, companyName, companyWebsite); const parameters: SendInvoiceParams = { senderWorkspaceID, @@ -3621,6 +3672,8 @@ function sendInvoice( reportPreviewReportActionID, transactionID, transactionThreadReportID, + companyName, + companyWebsite, ...(invoiceChatReport?.reportID ? {receiverInvoiceRoomID: invoiceChatReport.reportID} : {receiverEmail: receiver.login ?? ''}), }; diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index ac975f10517..e841f483a3d 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -1,17 +1,20 @@ import {Str} from 'expensify-common'; import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as ValidationUtils from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; +import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -25,12 +28,17 @@ import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotF type IOURequestStepCompanyInfoProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; -function IOURequestStepCompanyInfo({route, transaction}: IOURequestStepCompanyInfoProps) { +function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepCompanyInfoProps) { const {backTo} = route.params; const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`); const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(transaction?.amount ?? 0), transaction?.currency); @@ -68,6 +76,11 @@ function IOURequestStepCompanyInfo({route, transaction}: IOURequestStepCompanyIn [translate], ); + const submit = (values: FormOnyxValues) => { + playSound(SOUNDS.DONE); + IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, undefined, policy, policyTags, policyCategories, values.companyName, values.companyWebsite); + }; + return ( { - playSound(SOUNDS.DONE); - }} + onSubmit={submit} validate={validate} submitButtonText={translate('iou.sendInvoice', {amount: formattedAmount})} enabledWhenOffline diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ba26a36638e..17abfe42fb8 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1274,6 +1274,15 @@ type PolicyReportField = { defaultExternalID?: string | null; }; +/** Policy invoicing details */ +type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{ + /** Stripe Connect company name */ + companyName?: string; + + /** Stripe Connect company website */ + companyWebsite?: string; +}>; + /** Names of policy features */ type PolicyFeatureName = ValueOf; @@ -1429,6 +1438,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< */ isTaxTrackingEnabled?: boolean; + /** Policy invoicing details */ + invoice?: PolicyInvoicingDetails; + /** Tax data */ tax?: { /** Whether or not the policy has tax tracking enabled */ From 358bdf24c0977d158549cb9d1d77cca56bdd553f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jul 2024 14:24:08 +0200 Subject: [PATCH 5/7] Lint fix --- src/components/MoneyRequestConfirmationList.tsx | 5 ++++- src/libs/actions/Policy/Policy.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ac79278e9c9..458fabb684e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -389,7 +389,7 @@ function MoneyRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount, isTypeInvoice]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, policy, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount, isTypeInvoice]); const onSplitShareChange = useCallback( (accountID: number, value: number) => { @@ -746,6 +746,9 @@ function MoneyRequestConfirmationList({ iouAmount, onConfirm, shouldPlaySound, + transactionID, + reportID, + policy, ], ); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8b2c4b4255e..d823fadde5b 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -204,6 +204,7 @@ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefine } /** Check if the policy has invoicing company details */ +// eslint-disable-next-line react/no-unused-prop-types function hasInvoicingDetails(policy: OnyxEntry): boolean { // TODO: uncomment when invoicing details inside a policy are supported. // return !!policy.invoice.companyName && !!policy.invoice.companyWebsite; From 9ac2138dfb6dec2b803f9c9cb9b0c2eb89f91f52 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jul 2024 14:36:08 +0200 Subject: [PATCH 6/7] Small lint fix --- src/libs/actions/Policy/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d823fadde5b..d69892014a7 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -204,7 +204,7 @@ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefine } /** Check if the policy has invoicing company details */ -// eslint-disable-next-line react/no-unused-prop-types +// eslint-disable-next-line react/no-unused-prop-types,@typescript-eslint/no-unused-vars function hasInvoicingDetails(policy: OnyxEntry): boolean { // TODO: uncomment when invoicing details inside a policy are supported. // return !!policy.invoice.companyName && !!policy.invoice.companyWebsite; From 1a8c4036b32dc2d636d313b08e78895ae2e2ccdc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 26 Jul 2024 09:18:35 +0200 Subject: [PATCH 7/7] Minor improvements --- src/libs/ValidationUtils.ts | 2 +- src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 14c5e493cb2..bedcbb5eb26 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -244,7 +244,7 @@ function isValidWebsite(url: string): boolean { /** Checks if the domain is public */ function isPublicDomain(domain: string): boolean { - return PUBLIC_DOMAINS.some((publicDomain) => publicDomain === domain); + return PUBLIC_DOMAINS.some((publicDomain) => publicDomain === domain.toLowerCase()); } function validateIdentity(identity: Record): Record { diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index e841f483a3d..b1454b76aca 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -46,11 +46,7 @@ function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepC const DOMAIN_BASE_REGEX = '^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)'; const match = String(url).match(DOMAIN_BASE_REGEX); - if (!match) { - return undefined; - } - - return match[1]; + return match?.[1]; }; const validate = useCallback(