From 8e32d548c1fc6cb3c4ebf7455523b4f1e8429cd7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 25 Jul 2024 12:49:32 +0200 Subject: [PATCH 1/7] Implement WorkspaceInvoicingDetailsSection component --- src/languages/en.ts | 6 ++ src/languages/es.ts | 6 ++ .../WorkspaceInvoicingDetailsSection.tsx | 55 +++++++++++++++++++ src/types/onyx/Policy.ts | 12 ++++ 4 files changed, 79 insertions(+) create mode 100644 src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index b37ad15004e4..5fa10dcbfcb5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -710,6 +710,8 @@ 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.', + 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` : '' @@ -3250,6 +3252,10 @@ export default { viewUnpaidInvoices: 'View unpaid invoices', sendInvoice: 'Send invoice', sendFrom: 'Send from', + invoicingDetails: 'Invoicing details', + invoicingDetailsDescription: 'This info will appear on your invoices.', + companyName: 'Company name', + companyWebsite: 'Company website', paymentMethods: { personal: 'Personal', business: 'Business', diff --git a/src/languages/es.ts b/src/languages/es.ts index 54c97e41afb1..ba14d2cc0fbe 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -705,6 +705,8 @@ 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.', + 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` : '' @@ -3304,6 +3306,10 @@ export default { viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', sendFrom: 'Enviar desde', + invoicingDetails: 'Detalles de facturación', + invoicingDetailsDescription: 'Esta información aparecerá en sus facturas.', + companyName: 'Nombre de la empresa', + companyWebsite: 'Sitio web de la empresa', paymentMethods: { personal: 'Personal', business: 'Empresas', diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx new file mode 100644 index 000000000000..3eebe026d60a --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx @@ -0,0 +1,55 @@ +import React, {useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Section from '@components/Section'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type WorkspaceInvoicingDetailsSectionProps = { + /** The current policy ID */ + policyID: string; +}; + +function WorkspaceInvoicingDetailsSection({policyID}: WorkspaceInvoicingDetailsSectionProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + + const horizontalPadding = useMemo(() => (shouldUseNarrowLayout ? styles.ph5 : styles.ph8), [shouldUseNarrowLayout, styles]); + + return ( +
+ {}} + style={horizontalPadding} + /> + {}} + style={horizontalPadding} + /> +
+ ); +} + +WorkspaceInvoicingDetailsSection.displayName = 'WorkspaceInvoicingDetailsSection'; + +export default WorkspaceInvoicingDetailsSection; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ba26a36638e4..17abfe42fb85 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 46d1c931e5e9dd434b440c4582b7766334d2aecc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 25 Jul 2024 17:03:04 +0200 Subject: [PATCH 2/7] Implement pages to update company name and company website --- src/ONYXKEYS.ts | 6 ++ src/ROUTES.ts | 8 ++ src/SCREENS.ts | 2 + .../ModalStackNavigators/index.tsx | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 6 ++ src/libs/Navigation/types.ts | 6 ++ .../WorkspaceInvoicingDetailsName.tsx | 78 ++++++++++++++++++ .../WorkspaceInvoicingDetailsSection.tsx | 6 +- .../WorkspaceInvoicingDetailsWebsite.tsx | 80 +++++++++++++++++++ .../form/WorkspaceInvoicesCompanyNameForm.ts | 18 +++++ .../WorkspaceInvoicesCompanyWebsiteForm.ts | 18 +++++ src/types/form/index.ts | 2 + 13 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx create mode 100644 src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx create mode 100644 src/types/form/WorkspaceInvoicesCompanyNameForm.ts create mode 100644 src/types/form/WorkspaceInvoicesCompanyWebsiteForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 00f37508612d..a66c5bfbcb53 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -572,6 +572,10 @@ const ONYXKEYS = { WORKSPACE_TAX_NAME_FORM_DRAFT: 'workspaceTaxNameFormDraft', WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm', WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', + WORKSPACE_INVOICES_COMPANY_NAME_FORM: 'workspaceInvoicesCompanyNameForm', + WORKSPACE_INVOICES_COMPANY_NAME_FORM_DRAFT: 'workspaceInvoicesCompanyNameFormDraft', + WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM: 'workspaceInvoicesCompanyWebsiteForm', + WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM_DRAFT: 'workspaceInvoicesCompanyWebsiteFormDraft', NEW_CHAT_NAME_FORM: 'newChatNameForm', NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', @@ -658,6 +662,8 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CODE_FORM]: FormTypes.WorkspaceTaxCodeForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; + [ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM]: FormTypes.WorkspaceInvoicesCompanyNameForm; + [ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_WEBSITE_FORM]: FormTypes.WorkspaceInvoicesCompanyWebsiteForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 827e0f3b748d..0c1a124805ec 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -676,6 +676,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/invoices', getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices` as const, }, + WORKSPACE_INVOICES_COMPANY_NAME: { + route: 'settings/workspaces/:policyID/invoices/company-name', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices/company-name` as const, + }, + WORKSPACE_INVOICES_COMPANY_WEBSITE: { + route: 'settings/workspaces/:policyID/invoices/company-website', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices/company-website` as const, + }, WORKSPACE_TRAVEL: { route: 'settings/workspaces/:policyID/travel', getRoute: (policyID: string) => `settings/workspaces/${policyID}/travel` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 74d4a628e696..864828abc913 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -362,6 +362,8 @@ const SCREENS = { EXPENSIFY_CARD_SETTINGS_FREQUENCY: 'Workspace_ExpensifyCard_Settings_Frequency', BILLS: 'Workspace_Bills', INVOICES: 'Workspace_Invoices', + INVOICES_COMPANY_NAME: 'Workspace_Invoices_Company_Name', + INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website', TRAVEL: 'Workspace_Travel', MEMBERS: 'Workspace_Members', INVITE: 'Workspace_Invite', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c90a070a6a27..b9a23abbef72 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -418,6 +418,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default, [SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default, [SCREENS.WORKSPACE.TAX_CODE]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default, + [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default, + [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index c27ccaab5176..942a23068979 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -159,6 +159,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE, SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], + [SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE], [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [ SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4ce867884e32..c8e347d9f3cb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -468,6 +468,12 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.SHARE]: { path: ROUTES.WORKSPACE_PROFILE_SHARE.route, }, + [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: { + path: ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.route, + }, + [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: { + path: ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.route, + }, [SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_LIMIT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fadce5727fce..9268b7973d01 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -665,6 +665,12 @@ type SettingsNavigatorParamList = { policyID: string; taxID: string; }; + [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: { + policyID: string; + }; + [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: { + policyID: string; + }; [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { policyID: string; }; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx new file mode 100644 index 000000000000..d2f38b0600e2 --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx @@ -0,0 +1,78 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React 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 HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceInvoicesCompanyNameForm'; + +type WorkspaceInvoicingDetailsNameProps = StackScreenProps; + +function WorkspaceInvoicingDetailsName({route}: WorkspaceInvoicingDetailsNameProps) { + const {policyID} = route.params; + + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const styles = useThemeStyles(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const submit = (values: FormOnyxValues) => { + // TODO: add API call + Navigation.goBack(); + }; + + const validate = (values: FormOnyxValues): FormInputErrors => + ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_NAME]); + + return ( + + + + + + + + + ); +} + +WorkspaceInvoicingDetailsName.displayName = 'WorkspaceInvoicingDetailsName'; + +export default WorkspaceInvoicingDetailsName; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx index 3eebe026d60a..c734e4b6d517 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx @@ -5,7 +5,9 @@ import Section from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; type WorkspaceInvoicingDetailsSectionProps = { /** The current policy ID */ @@ -35,7 +37,7 @@ function WorkspaceInvoicingDetailsSection({policyID}: WorkspaceInvoicingDetailsS shouldShowRightIcon title={policy?.invoice?.companyName} description={translate('workspace.invoices.companyName')} - onPress={() => {}} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.getRoute(policyID))} style={horizontalPadding} /> {}} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.getRoute(policyID))} style={horizontalPadding} /> diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx new file mode 100644 index 000000000000..d23983d4ff8b --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx @@ -0,0 +1,80 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React 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 HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceInvoicesCompanyWebsiteForm'; + +type WorkspaceInvoicingDetailsWebsiteProps = StackScreenProps; + +function WorkspaceInvoicingDetailsWebsite({route}: WorkspaceInvoicingDetailsWebsiteProps) { + const {policyID} = route.params; + + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const styles = useThemeStyles(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const submit = (values: FormOnyxValues) => { + // TODO: add API call + Navigation.goBack(); + }; + + const validate = ( + values: FormOnyxValues, + ): FormInputErrors => ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_WEBSITE]); + + return ( + + + + + + + + + ); +} + +WorkspaceInvoicingDetailsWebsite.displayName = 'WorkspaceInvoicingDetailsWebsite'; + +export default WorkspaceInvoicingDetailsWebsite; diff --git a/src/types/form/WorkspaceInvoicesCompanyNameForm.ts b/src/types/form/WorkspaceInvoicesCompanyNameForm.ts new file mode 100644 index 000000000000..d3e6dd606cd5 --- /dev/null +++ b/src/types/form/WorkspaceInvoicesCompanyNameForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + COMPANY_NAME: 'companyName', +} as const; + +type InputID = ValueOf; + +type WorkspaceInvoicesCompanyNameForm = Form< + InputID, + { + [INPUT_IDS.COMPANY_NAME]: string; + } +>; + +export type {WorkspaceInvoicesCompanyNameForm}; +export default INPUT_IDS; diff --git a/src/types/form/WorkspaceInvoicesCompanyWebsiteForm.ts b/src/types/form/WorkspaceInvoicesCompanyWebsiteForm.ts new file mode 100644 index 000000000000..3f3bba19d96e --- /dev/null +++ b/src/types/form/WorkspaceInvoicesCompanyWebsiteForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + COMPANY_WEBSITE: 'companyWebsite', +} as const; + +type InputID = ValueOf; + +type WorkspaceInvoicesCompanyWebsiteForm = Form< + InputID, + { + [INPUT_IDS.COMPANY_WEBSITE]: string; + } +>; + +export type {WorkspaceInvoicesCompanyWebsiteForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index a6ef63329006..f2a4e8c2395a 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -49,6 +49,8 @@ export type {WorkspaceTaxNameForm} from './WorkspaceTaxNameForm'; export type {WorkspaceTaxValueForm} from './WorkspaceTaxValueForm'; export type {WorkspaceTaxCustomName} from './WorkspaceTaxCustomName'; export type {WorkspaceTaxCodeForm} from './WorkspaceTaxCodeForm'; +export type {WorkspaceInvoicesCompanyNameForm} from './WorkspaceInvoicesCompanyNameForm'; +export type {WorkspaceInvoicesCompanyWebsiteForm} from './WorkspaceInvoicesCompanyWebsiteForm'; export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; export type {PolicyDistanceRateTaxReclaimableOnEditForm} from './PolicyDistanceRateTaxReclaimableOnEditForm'; export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm'; From 9509013e9b5edc296f1264f8d2e763bfc3625253 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 26 Jul 2024 17:09:59 +0200 Subject: [PATCH 3/7] Improve validation, add TODOs --- src/CONST.ts | 1 + src/libs/Url.ts | 10 ++++++- src/libs/ValidationUtils.ts | 8 +++++- .../WorkspaceInvoicingDetailsName.tsx | 5 ++-- .../WorkspaceInvoicingDetailsWebsite.tsx | 27 ++++++++++++++++--- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index c7a6741da314..8b924cacd524 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2048,6 +2048,7 @@ const CONST = { ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled', ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled', ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled', + ARE_INVOICES_ENABLED: 'areInvoicesEnabled', ARE_TAXES_ENABLED: 'tax', }, DEFAULT_CATEGORIES: [ diff --git a/src/libs/Url.ts b/src/libs/Url.ts index 970e6b3ed195..6b3523ab11e8 100644 --- a/src/libs/Url.ts +++ b/src/libs/Url.ts @@ -70,4 +70,12 @@ function hasURL(text: string) { return urlPattern.test(text); } -export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash}; +function extractUrlDomain(url: string): string | undefined { + const DOMAIN_BASE_REGEX = '^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)'; + + const match = String(url).match(DOMAIN_BASE_REGEX); + + return match?.[1]; +} + +export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash, extractUrlDomain}; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 64cae69e0b15..bedcbb5eb264 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.toLowerCase()); +} + 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/workspace/invoices/WorkspaceInvoicingDetailsName.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx index d2f38b0600e2..fff7c592baaa 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx @@ -31,7 +31,7 @@ function WorkspaceInvoicingDetailsName({route}: WorkspaceInvoicingDetailsNamePro // eslint-disable-next-line @typescript-eslint/no-unused-vars const submit = (values: FormOnyxValues) => { - // TODO: add API call + // TODO: implement UpdateInvoiceCompanyName API call when it's supported Navigation.goBack(); }; @@ -42,7 +42,8 @@ function WorkspaceInvoicingDetailsName({route}: WorkspaceInvoicingDetailsNamePro ) => { - // TODO: add API call + // TODO: implement UpdateInvoiceCompanyWebsite API call when it's supported Navigation.goBack(); }; const validate = ( values: FormOnyxValues, - ): FormInputErrors => ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_WEBSITE]); + ): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_WEBSITE]); + + if (values.companyWebsite) { + if (!ValidationUtils.isValidWebsite(values.companyWebsite)) { + errors.companyWebsite = translate('bankAccount.error.website'); + } else { + const domain = Url.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; + }; return ( Date: Fri, 26 Jul 2024 17:41:45 +0200 Subject: [PATCH 4/7] Fix TS errors --- src/types/onyx/Policy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 17abfe42fb85..ee553381454d 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1477,6 +1477,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the Expensify Card feature is enabled */ areExpensifyCardsEnabled?: boolean; + /** Whether the Invoices feature is enabled */ + areInvoicesEnabled?: boolean; + /** Whether the workflows feature is enabled */ areWorkflowsEnabled?: boolean; From 4650c27ea9486c4d0cb6faf76afdba2cee8e900e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Aug 2024 09:57:41 +0200 Subject: [PATCH 5/7] Put extractUrlDomain function to Url utils --- src/CONST.ts | 1 + src/libs/Url.ts | 6 ++---- .../iou/request/step/IOURequestStepCompanyInfo.tsx | 10 ++-------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 594463bb14ea..bac9e803f54c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2320,6 +2320,7 @@ const CONST = { CARD_SECURITY_CODE: /^[0-9]{3,4}$/, CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, ROOM_NAME: /^#[\p{Ll}0-9-]{1,100}$/u, + DOMAIN_BASE: '^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)', // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, diff --git a/src/libs/Url.ts b/src/libs/Url.ts index 6b3523ab11e8..ef595c484bae 100644 --- a/src/libs/Url.ts +++ b/src/libs/Url.ts @@ -1,4 +1,5 @@ import 'react-native-url-polyfill/auto'; +import CONST from '@src/CONST'; import type {Route} from '@src/ROUTES'; /** @@ -71,10 +72,7 @@ function hasURL(text: string) { } function extractUrlDomain(url: string): string | undefined { - const DOMAIN_BASE_REGEX = '^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)'; - - const match = String(url).match(DOMAIN_BASE_REGEX); - + const match = String(url).match(CONST.REGEX.DOMAIN_BASE); return match?.[1]; } diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index b1454b76aca6..f1d4277fa457 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -12,6 +12,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 Url from '@libs/Url'; import * as ValidationUtils from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import * as IOU from '@userActions/IOU'; @@ -42,13 +43,6 @@ function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepC 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); - - return match?.[1]; - }; - const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_NAME, INPUT_IDS.COMPANY_WEBSITE]); @@ -57,7 +51,7 @@ function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepC if (!ValidationUtils.isValidWebsite(values.companyWebsite)) { errors.companyWebsite = translate('bankAccount.error.website'); } else { - const domain = extractUrlDomain(values.companyWebsite); + const domain = Url.extractUrlDomain(values.companyWebsite); if (!domain || !Str.isValidDomainName(domain)) { errors.companyWebsite = translate('iou.invalidDomainError'); From f62e67e0e5229bba5eb36dba938fd6cd559a4062 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Aug 2024 11:27:42 +0200 Subject: [PATCH 6/7] Correct translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 5c81edfeaa0f..19b3ae7c06d9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3352,7 +3352,7 @@ export default { sendInvoice: 'Enviar factura', sendFrom: 'Enviar desde', invoicingDetails: 'Detalles de facturación', - invoicingDetailsDescription: 'Esta información aparecerá en sus facturas.', + invoicingDetailsDescription: 'Esta información aparecerá en tus facturas.', companyName: 'Nombre de la empresa', companyWebsite: 'Sitio web de la empresa', paymentMethods: { From b329c5cddbd14cd62f7ae54b12c44d38c8490f93 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Aug 2024 11:36:13 +0200 Subject: [PATCH 7/7] Fix TS issue --- src/types/onyx/Policy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 62f613b7b3fa..9724229361ec 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1495,9 +1495,6 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the Expensify Card feature is enabled */ areExpensifyCardsEnabled?: boolean; - /** Whether the Invoices feature is enabled */ - areInvoicesEnabled?: boolean; - /** Whether the workflows feature is enabled */ areWorkflowsEnabled?: boolean;