Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,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: [
Expand Down Expand Up @@ -2318,6 +2319,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,
Expand Down
6 changes: 6 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,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',
Expand Down Expand Up @@ -673,6 +677,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;
Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,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,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,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',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3308,6 +3308,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',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3359,6 +3359,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 tus facturas.',
companyName: 'Nombre de la empresa',
companyWebsite: 'Sitio web de la empresa',
paymentMethods: {
personal: 'Personal',
business: 'Empresas',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.TAX_VALUE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/ValuePage').default,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.TAX_CODE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require<ReactComponentModule>('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require<ReactComponentModule>('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
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,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ const config: LinkingOptions<RootStackParamList>['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,
},
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
8 changes: 7 additions & 1 deletion src/libs/Url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'react-native-url-polyfill/auto';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';

/**
Expand Down Expand Up @@ -70,4 +71,9 @@ function hasURL(text: string) {
return urlPattern.test(text);
}

export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash};
function extractUrlDomain(url: string): string | undefined {
const match = String(url).match(CONST.REGEX.DOMAIN_BASE);
return match?.[1];
}

export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash, extractUrlDomain};
10 changes: 2 additions & 8 deletions src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<typeof ONYXKEYS.FORMS.MONEY_REQUEST_COMPANY_INFO_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.MONEY_REQUEST_COMPANY_INFO_FORM> => {
const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_NAME, INPUT_IDS.COMPANY_WEBSITE]);
Expand All @@ -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');
Expand Down
79 changes: 79 additions & 0 deletions src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.INVOICES_COMPANY_NAME>;

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<typeof ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM>) => {
// TODO: implement UpdateInvoiceCompanyName API call when it's supported
Navigation.goBack();
};

const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM> =>
ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMPANY_NAME]);

return (
<AccessOrNotFoundWrapper
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
// TODO: uncomment when CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED is supported
// featureName={CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED}
>
<ScreenWrapper
testID={WorkspaceInvoicingDetailsName.displayName}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
>
<HeaderWithBackButton title={translate('workspace.invoices.companyName')} />
<FormProvider
formID={ONYXKEYS.FORMS.WORKSPACE_INVOICES_COMPANY_NAME_FORM}
submitButtonText={translate('common.save')}
onSubmit={submit}
style={[styles.flex1, styles.mh5]}
enabledWhenOffline
validate={validate}
>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.COMPANY_NAME}
label={translate('workspace.invoices.companyName')}
accessibilityLabel={translate('workspace.invoices.companyName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={policy?.invoice?.companyName}
ref={inputCallbackRef}
/>
</FormProvider>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

WorkspaceInvoicingDetailsName.displayName = 'WorkspaceInvoicingDetailsName';

export default WorkspaceInvoicingDetailsName;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 Navigation from '@navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

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 (
<Section
title={translate('workspace.invoices.invoicingDetails')}
subtitle={translate('workspace.invoices.invoicingDetailsDescription')}
containerStyles={[styles.ph0, shouldUseNarrowLayout ? styles.pt5 : styles.pt8]}
subtitleStyles={horizontalPadding}
titleStyles={[styles.textStrong, horizontalPadding]}
childrenStyles={styles.pt5}
subtitleMuted
>
<MenuItemWithTopDescription
key={translate('workspace.invoices.companyName')}
shouldShowRightIcon
title={policy?.invoice?.companyName}
description={translate('workspace.invoices.companyName')}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.getRoute(policyID))}
style={horizontalPadding}
/>
<MenuItemWithTopDescription
key={translate('workspace.invoices.companyWebsite')}
shouldShowRightIcon
title={policy?.invoice?.companyWebsite}
description={translate('workspace.invoices.companyWebsite')}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.getRoute(policyID))}
style={horizontalPadding}
/>
</Section>
);
}

WorkspaceInvoicingDetailsSection.displayName = 'WorkspaceInvoicingDetailsSection';

export default WorkspaceInvoicingDetailsSection;
Loading