Skip to content

Commit

Permalink
Merge pull request #55300 from Expensify/cristi_domainSelection-when-…
Browse files Browse the repository at this point in the history
…provisionTravel

Domain selection when enabling travel for workspaces with admins from multiple domains
  • Loading branch information
tgolen authored Jan 27, 2025
2 parents 9468d26 + 7c225bb commit e0da849
Show file tree
Hide file tree
Showing 31 changed files with 694 additions and 230 deletions.
7 changes: 7 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ const CONST = {
ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`,
WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`,
BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`,
EXPENSIFY_APPROVED_PROGRAM_URL: `${USE_EXPENSIFY_URL}/accountants-program`,
},
OLDDOT_URLS: {
ADMIN_POLICIES_URL: 'admin_policies',
Expand Down Expand Up @@ -6541,6 +6542,12 @@ const CONST = {
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
},
SMART_BANNER_HEIGHT: 152,
TRAVEL: {
DEFAULT_DOMAIN: 'domain',
PROVISIONING: {
ERROR_PERMISSION_DENIED: 'permissionDenied',
},
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ const ONYXKEYS = {
/** Corpay onboarding fields used in steps 3-5 in the global reimbursements */
CORPAY_ONBOARDING_FIELDS: 'corpayOnboardingFields',

/** Information about travel provisioning process */
TRAVEL_PROVISIONING: 'travelProvisioning',

/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
Expand Down Expand Up @@ -1055,6 +1058,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
[ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;

Expand Down
11 changes: 10 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,10 @@ const ROUTES = {
getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo),
},
TRAVEL_MY_TRIPS: 'travel',
TRAVEL_TCS: 'travel/terms',
TRAVEL_TCS: {
route: 'travel/terms/:domain/accept',
getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`travel/terms/${domain}/accept`, backTo),
},
TRACK_TRAINING_MODAL: 'track-training',
TRAVEL_TRIP_SUMMARY: {
route: 'r/:reportID/trip/:transactionID',
Expand All @@ -1591,6 +1594,12 @@ const ROUTES = {
getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) =>
getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo),
},
TRAVEL_DOMAIN_SELECTOR: 'travel/domain-selector',
TRAVEL_DOMAIN_PERMISSION_INFO: {
route: 'travel/domain-permission/:domain/info',
getRoute: (domain?: string, backTo?: string) => getUrlWithBackToParam(`travel/domain-permission/${domain}/info`, backTo),
},
TRAVEL_PUBLIC_DOMAIN_ERROR: 'travel/public-domain-error',
ONBOARDING_ROOT: {
route: 'onboarding',
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo),
Expand Down
3 changes: 3 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const SCREENS = {
TCS: 'Travel_TCS',
TRIP_SUMMARY: 'Travel_TripSummary',
TRIP_DETAILS: 'Travel_TripDetails',
DOMAIN_SELECTOR: 'Travel_DomainSelector',
DOMAIN_PERMISSION_INFO: 'Travel_DomainPermissionInfo',
PUBLIC_DOMAIN_ERROR: 'Travel_PublicDomainError',
},
SEARCH: {
CENTRAL_PANE: 'Search_Central_Pane',
Expand Down
92 changes: 92 additions & 0 deletions src/components/SelectionList/TravelDomainListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, {useCallback} from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import SelectCircle from '@components/SelectCircle';
import TextWithTooltip from '@components/TextWithTooltip';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {BaseListItemProps, ListItem} from './types';

type AdditionalDomainItemProps = {
value?: string;
isRecommended?: boolean;
};

type DomainItemProps<TItem extends ListItem> = BaseListItemProps<TItem & AdditionalDomainItemProps> & {shouldHighlightSelectedItem?: boolean};

function TravelDomainListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
isDisabled,
onSelectRow,
onCheckboxPress,
onFocus,
shouldSyncFocus,
shouldHighlightSelectedItem,
}: DomainItemProps<TItem>) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const handleCheckboxPress = useCallback(() => {
if (onCheckboxPress) {
onCheckboxPress(item);
} else {
onSelectRow(item);
}
}, [item, onCheckboxPress, onSelectRow]);
const showRecommendedTag = item.isRecommended ?? false;

return (
<BaseListItem
pressableStyle={[[shouldHighlightSelectedItem && item.isSelected && styles.activeComponentBG]]}
item={item}
wrapperStyle={[styles.flex1, styles.sidebarLinkInner, styles.userSelectNone, styles.optionRow, styles.justifyContentBetween]}
isFocused={isFocused}
isDisabled={isDisabled}
showTooltip={showTooltip}
canSelectMultiple
onSelectRow={onSelectRow}
keyForList={item.keyForList}
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
>
<>
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<PressableWithFeedback
onPress={handleCheckboxPress}
disabled={isDisabled}
role={CONST.ROLE.BUTTON}
accessibilityLabel={item.text ?? ''}
style={[styles.mr2, styles.optionSelectCircle]}
>
<SelectCircle
isChecked={item.isSelected ?? false}
selectCircleStyles={styles.ml0}
/>
</PressableWithFeedback>
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<TextWithTooltip
shouldShowTooltip={showTooltip}
text={item.text ?? ''}
style={[
styles.optionDisplayName,
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
item.isBold !== false && styles.sidebarLinkTextBold,
styles.pre,
]}
/>
</View>
</View>
{showRecommendedTag && <Badge text={translate('travel.domainSelector.recommended')} />}
</>
</BaseListItem>
);
}

TravelDomainListItem.displayName = 'TravelDomainListItem';

export default TravelDomainListItem;
4 changes: 3 additions & 1 deletion src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type ReportListItem from './Search/ReportListItem';
import type SearchQueryListItem from './Search/SearchQueryListItem';
import type TransactionListItem from './Search/TransactionListItem';
import type TableListItem from './TableListItem';
import type TravelDomainListItem from './TravelDomainListItem';
import type UserListItem from './UserListItem';

type TRightHandSideComponent<TItem extends ListItem> = {
Expand Down Expand Up @@ -363,7 +364,8 @@ type ValidListItem =
| typeof ReportListItem
| typeof ChatListItem
| typeof SearchQueryListItem
| typeof SearchRouterItem;
| typeof SearchRouterItem
| typeof TravelDomainListItem;

type Section<TItem extends ListItem> = {
/** Title of the section */
Expand Down
17 changes: 17 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2566,6 +2566,23 @@ const translations = {
departs: 'Departs',
errorMessage: 'Something went wrong. Please try again later.',
phoneError: 'To book travel, your default contact method must be a valid email',
domainSelector: {
title: 'Domain',
subtitle: 'Choose a domain for Expensify Travel setup.',
recommended: 'Recommended',
},
domainPermissionInfo: {
title: 'Domain',
restrictionPrefix: `You don't have permission to enable Expensify Travel for the domain`,
restrictionSuffix: `You'll need to ask someone from that domain to enable travel instead.`,
accountantInvitationPrefix: `If you're an accountant, consider joining the`,
accountantInvitationLink: `ExpensifyApproved! accountants program`,
accountantInvitationSuffix: `to enable travel for this domain.`,
},
publicDomainError: {
title: 'Get started with Expensify Travel',
message: `You'll need to use your work email (e.g., [email protected]) with Expensify Travel, not your personal email (e.g., [email protected]).`,
},
},
workspace: {
common: {
Expand Down
17 changes: 17 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2590,6 +2590,23 @@ const translations = {
departs: 'Sale',
errorMessage: 'Ha ocurrido un error. Por favor, inténtalo mas tarde.',
phoneError: 'Para reservar viajes, tu método de contacto predeterminado debe ser un correo electrónico válido',
domainSelector: {
title: 'Dominio',
subtitle: 'Elige un dominio para configurar Expensify Travel.',
recommended: 'Recomendado',
},
domainPermissionInfo: {
title: 'Dominio',
restrictionPrefix: `No tienes permiso para habilitar Expensify Travel para el dominio`,
restrictionSuffix: `Tendrás que pedir a alguien de ese dominio que habilite Travel por ti.`,
accountantInvitationPrefix: `Si eres contador, considera unirte al`,
accountantInvitationLink: `programa de contadores ExpensifyApproved!`,
accountantInvitationSuffix: `para habilitar Travel para este dominio.`,
},
publicDomainError: {
title: 'Comienza con Expensify Travel',
message: 'Tendrás que usar tu correo electrónico laboral (por ejemplo, [email protected]) con Expensify Travel, no tu correo personal (por ejemplo, [email protected]).',
},
},
workspace: {
common: {
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/AcceptSpotnanaTermsParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type AcceptSpotnanaTermsParams = {
domain?: string;
};

export default AcceptSpotnanaTermsParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,4 @@ export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDel
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';
4 changes: 1 addition & 3 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SHARE_TRACKED_EXPENSE]: Parameters.ShareTrackedExpenseParams;
[WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams;
[WRITE_COMMANDS.DISMISS_VIOLATION]: Parameters.DismissViolationParams;
[WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null;
[WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: Parameters.AcceptSpotnanaTermsParams;
[WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams;
[WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams;
[WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams;
Expand Down Expand Up @@ -1042,7 +1042,6 @@ type ReadCommandParameters = {
};

const SIDE_EFFECT_REQUEST_COMMANDS = {
ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms',
AUTHENTICATE_PUSHER: 'AuthenticatePusher',
GENERATE_SPOTNANA_TOKEN: 'GenerateSpotnanaToken',
GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages',
Expand Down Expand Up @@ -1075,7 +1074,6 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams;
[SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBP]: Parameters.AddPaymentCardParams;
[SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null;
[SIDE_EFFECT_REQUEST_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
[SIDE_EFFECT_REQUEST_COMMANDS.CONNECT_AS_DELEGATE]: Parameters.ConnectAsDelegateParams;
[SIDE_EFFECT_REQUEST_COMMANDS.DISCONNECT_AS_DELEGATE]: EmptyObject;
Expand Down
12 changes: 0 additions & 12 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type IconAsset from '@src/types/utils/IconAsset';
import localeCompare from './LocaleCompare';
import {translateLocal} from './Localize';
import {getDisplayNameOrDefault} from './PersonalDetailsUtils';
import {getPolicy} from './PolicyUtils';

let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {};
Onyx.connect({
Expand Down Expand Up @@ -448,16 +447,6 @@ function getAllCardsForWorkspace(workspaceAccountID: number): CardList {
return cards;
}

const getDescriptionForPolicyDomainCard = (domainName: string): string => {
// A domain name containing a policyID indicates that this is a workspace feed
const policyID = domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME)?.[1];
if (policyID) {
const policy = getPolicy(policyID.toUpperCase());
return policy?.name ?? domainName;
}
return domainName;
};

const CUSTOM_FEEDS = [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX];

function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry<CardFeeds>): CompanyCardFeedWithNumber {
Expand Down Expand Up @@ -511,7 +500,6 @@ export {
getDefaultCardName,
mergeCardListWithWorkspaceFeeds,
isCard,
getDescriptionForPolicyDomainCard,
getAllCardsForWorkspace,
isCardIssued,
isCardHiddenFromSearch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ const TravelModalStackNavigator = createModalStackNavigator<TravelNavigatorParam
[SCREENS.TRAVEL.TCS]: () => require<ReactComponentModule>('../../../../pages/Travel/TravelTerms').default,
[SCREENS.TRAVEL.TRIP_SUMMARY]: () => require<ReactComponentModule>('../../../../pages/Travel/TripSummaryPage').default,
[SCREENS.TRAVEL.TRIP_DETAILS]: () => require<ReactComponentModule>('../../../../pages/Travel/TripDetailsPage').default,
[SCREENS.TRAVEL.DOMAIN_SELECTOR]: () => require<ReactComponentModule>('../../../../pages/Travel/DomainSelectorPage').default,
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: () => require<ReactComponentModule>('../../../../pages/Travel/DomainPermissionInfoPage').default,
[SCREENS.TRAVEL.PUBLIC_DOMAIN_ERROR]: () => require<ReactComponentModule>('../../../../pages/Travel/PublicDomainErrorPage').default,
});

const SplitDetailsModalStackNavigator = createModalStackNavigator<SplitDetailsNavigatorParamList>({
Expand Down
5 changes: 4 additions & 1 deletion src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1394,14 +1394,17 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.RIGHT_MODAL.TRAVEL]: {
screens: {
[SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS,
[SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS,
[SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS.route,
[SCREENS.TRAVEL.TRIP_SUMMARY]: ROUTES.TRAVEL_TRIP_SUMMARY.route,
[SCREENS.TRAVEL.TRIP_DETAILS]: {
path: ROUTES.TRAVEL_TRIP_DETAILS.route,
parse: {
reservationIndex: (reservationIndex: string) => parseInt(reservationIndex, 10),
},
},
[SCREENS.TRAVEL.DOMAIN_SELECTOR]: ROUTES.TRAVEL_DOMAIN_SELECTOR,
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: ROUTES.TRAVEL_DOMAIN_PERMISSION_INFO.route,
[SCREENS.TRAVEL.PUBLIC_DOMAIN_ERROR]: ROUTES.TRAVEL_PUBLIC_DOMAIN_ERROR,
},
},
[SCREENS.RIGHT_MODAL.SEARCH_REPORT]: {
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 @@ -1504,6 +1504,12 @@ type TravelNavigatorParamList = {
reservationIndex: number;
backTo?: string;
};
[SCREENS.TRAVEL.TCS]: {
domain?: string;
};
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: {
domain: string;
};
};

type FullScreenNavigatorParamList = {
Expand Down
Loading

0 comments on commit e0da849

Please sign in to comment.