Skip to content

Commit e0da849

Browse files
authored
Merge pull request #55300 from Expensify/cristi_domainSelection-when-provisionTravel
Domain selection when enabling travel for workspaces with admins from multiple domains
2 parents 9468d26 + 7c225bb commit e0da849

31 files changed

+694
-230
lines changed

src/CONST.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ const CONST = {
982982
ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`,
983983
WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`,
984984
BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`,
985+
EXPENSIFY_APPROVED_PROGRAM_URL: `${USE_EXPENSIFY_URL}/accountants-program`,
985986
},
986987
OLDDOT_URLS: {
987988
ADMIN_POLICIES_URL: 'admin_policies',
@@ -6541,6 +6542,12 @@ const CONST = {
65416542
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
65426543
},
65436544
SMART_BANNER_HEIGHT: 152,
6545+
TRAVEL: {
6546+
DEFAULT_DOMAIN: 'domain',
6547+
PROVISIONING: {
6548+
ERROR_PERMISSION_DENIED: 'permissionDenied',
6549+
},
6550+
},
65446551
} as const;
65456552

65466553
type Country = keyof typeof CONST.ALL_COUNTRIES;

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,9 @@ const ONYXKEYS = {
467467
/** Corpay onboarding fields used in steps 3-5 in the global reimbursements */
468468
CORPAY_ONBOARDING_FIELDS: 'corpayOnboardingFields',
469469

470+
/** Information about travel provisioning process */
471+
TRAVEL_PROVISIONING: 'travelProvisioning',
472+
470473
/** Collection Keys */
471474
COLLECTION: {
472475
DOWNLOAD: 'download_',
@@ -1055,6 +1058,7 @@ type OnyxValuesMapping = {
10551058
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
10561059
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
10571060
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
1061+
[ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning;
10581062
};
10591063
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
10601064

src/ROUTES.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1580,7 +1580,10 @@ const ROUTES = {
15801580
getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo),
15811581
},
15821582
TRAVEL_MY_TRIPS: 'travel',
1583-
TRAVEL_TCS: 'travel/terms',
1583+
TRAVEL_TCS: {
1584+
route: 'travel/terms/:domain/accept',
1585+
getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`travel/terms/${domain}/accept`, backTo),
1586+
},
15841587
TRACK_TRAINING_MODAL: 'track-training',
15851588
TRAVEL_TRIP_SUMMARY: {
15861589
route: 'r/:reportID/trip/:transactionID',
@@ -1591,6 +1594,12 @@ const ROUTES = {
15911594
getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) =>
15921595
getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo),
15931596
},
1597+
TRAVEL_DOMAIN_SELECTOR: 'travel/domain-selector',
1598+
TRAVEL_DOMAIN_PERMISSION_INFO: {
1599+
route: 'travel/domain-permission/:domain/info',
1600+
getRoute: (domain?: string, backTo?: string) => getUrlWithBackToParam(`travel/domain-permission/${domain}/info`, backTo),
1601+
},
1602+
TRAVEL_PUBLIC_DOMAIN_ERROR: 'travel/public-domain-error',
15941603
ONBOARDING_ROOT: {
15951604
route: 'onboarding',
15961605
getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo),

src/SCREENS.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const SCREENS = {
3030
TCS: 'Travel_TCS',
3131
TRIP_SUMMARY: 'Travel_TripSummary',
3232
TRIP_DETAILS: 'Travel_TripDetails',
33+
DOMAIN_SELECTOR: 'Travel_DomainSelector',
34+
DOMAIN_PERMISSION_INFO: 'Travel_DomainPermissionInfo',
35+
PUBLIC_DOMAIN_ERROR: 'Travel_PublicDomainError',
3336
},
3437
SEARCH: {
3538
CENTRAL_PANE: 'Search_Central_Pane',
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, {useCallback} from 'react';
2+
import {View} from 'react-native';
3+
import Badge from '@components/Badge';
4+
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
5+
import SelectCircle from '@components/SelectCircle';
6+
import TextWithTooltip from '@components/TextWithTooltip';
7+
import useLocalize from '@hooks/useLocalize';
8+
import useThemeStyles from '@hooks/useThemeStyles';
9+
import CONST from '@src/CONST';
10+
import BaseListItem from './BaseListItem';
11+
import type {BaseListItemProps, ListItem} from './types';
12+
13+
type AdditionalDomainItemProps = {
14+
value?: string;
15+
isRecommended?: boolean;
16+
};
17+
18+
type DomainItemProps<TItem extends ListItem> = BaseListItemProps<TItem & AdditionalDomainItemProps> & {shouldHighlightSelectedItem?: boolean};
19+
20+
function TravelDomainListItem<TItem extends ListItem>({
21+
item,
22+
isFocused,
23+
showTooltip,
24+
isDisabled,
25+
onSelectRow,
26+
onCheckboxPress,
27+
onFocus,
28+
shouldSyncFocus,
29+
shouldHighlightSelectedItem,
30+
}: DomainItemProps<TItem>) {
31+
const styles = useThemeStyles();
32+
const {translate} = useLocalize();
33+
34+
const handleCheckboxPress = useCallback(() => {
35+
if (onCheckboxPress) {
36+
onCheckboxPress(item);
37+
} else {
38+
onSelectRow(item);
39+
}
40+
}, [item, onCheckboxPress, onSelectRow]);
41+
const showRecommendedTag = item.isRecommended ?? false;
42+
43+
return (
44+
<BaseListItem
45+
pressableStyle={[[shouldHighlightSelectedItem && item.isSelected && styles.activeComponentBG]]}
46+
item={item}
47+
wrapperStyle={[styles.flex1, styles.sidebarLinkInner, styles.userSelectNone, styles.optionRow, styles.justifyContentBetween]}
48+
isFocused={isFocused}
49+
isDisabled={isDisabled}
50+
showTooltip={showTooltip}
51+
canSelectMultiple
52+
onSelectRow={onSelectRow}
53+
keyForList={item.keyForList}
54+
onFocus={onFocus}
55+
shouldSyncFocus={shouldSyncFocus}
56+
>
57+
<>
58+
<View style={[styles.flexRow, styles.alignItemsCenter]}>
59+
<PressableWithFeedback
60+
onPress={handleCheckboxPress}
61+
disabled={isDisabled}
62+
role={CONST.ROLE.BUTTON}
63+
accessibilityLabel={item.text ?? ''}
64+
style={[styles.mr2, styles.optionSelectCircle]}
65+
>
66+
<SelectCircle
67+
isChecked={item.isSelected ?? false}
68+
selectCircleStyles={styles.ml0}
69+
/>
70+
</PressableWithFeedback>
71+
<View style={[styles.flexRow, styles.alignItemsCenter]}>
72+
<TextWithTooltip
73+
shouldShowTooltip={showTooltip}
74+
text={item.text ?? ''}
75+
style={[
76+
styles.optionDisplayName,
77+
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
78+
item.isBold !== false && styles.sidebarLinkTextBold,
79+
styles.pre,
80+
]}
81+
/>
82+
</View>
83+
</View>
84+
{showRecommendedTag && <Badge text={translate('travel.domainSelector.recommended')} />}
85+
</>
86+
</BaseListItem>
87+
);
88+
}
89+
90+
TravelDomainListItem.displayName = 'TravelDomainListItem';
91+
92+
export default TravelDomainListItem;

src/components/SelectionList/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type ReportListItem from './Search/ReportListItem';
3131
import type SearchQueryListItem from './Search/SearchQueryListItem';
3232
import type TransactionListItem from './Search/TransactionListItem';
3333
import type TableListItem from './TableListItem';
34+
import type TravelDomainListItem from './TravelDomainListItem';
3435
import type UserListItem from './UserListItem';
3536

3637
type TRightHandSideComponent<TItem extends ListItem> = {
@@ -363,7 +364,8 @@ type ValidListItem =
363364
| typeof ReportListItem
364365
| typeof ChatListItem
365366
| typeof SearchQueryListItem
366-
| typeof SearchRouterItem;
367+
| typeof SearchRouterItem
368+
| typeof TravelDomainListItem;
367369

368370
type Section<TItem extends ListItem> = {
369371
/** Title of the section */

src/languages/en.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,6 +2566,23 @@ const translations = {
25662566
departs: 'Departs',
25672567
errorMessage: 'Something went wrong. Please try again later.',
25682568
phoneError: 'To book travel, your default contact method must be a valid email',
2569+
domainSelector: {
2570+
title: 'Domain',
2571+
subtitle: 'Choose a domain for Expensify Travel setup.',
2572+
recommended: 'Recommended',
2573+
},
2574+
domainPermissionInfo: {
2575+
title: 'Domain',
2576+
restrictionPrefix: `You don't have permission to enable Expensify Travel for the domain`,
2577+
restrictionSuffix: `You'll need to ask someone from that domain to enable travel instead.`,
2578+
accountantInvitationPrefix: `If you're an accountant, consider joining the`,
2579+
accountantInvitationLink: `ExpensifyApproved! accountants program`,
2580+
accountantInvitationSuffix: `to enable travel for this domain.`,
2581+
},
2582+
publicDomainError: {
2583+
title: 'Get started with Expensify Travel',
2584+
message: `You'll need to use your work email (e.g., [email protected]) with Expensify Travel, not your personal email (e.g., [email protected]).`,
2585+
},
25692586
},
25702587
workspace: {
25712588
common: {

src/languages/es.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2590,6 +2590,23 @@ const translations = {
25902590
departs: 'Sale',
25912591
errorMessage: 'Ha ocurrido un error. Por favor, inténtalo mas tarde.',
25922592
phoneError: 'Para reservar viajes, tu método de contacto predeterminado debe ser un correo electrónico válido',
2593+
domainSelector: {
2594+
title: 'Dominio',
2595+
subtitle: 'Elige un dominio para configurar Expensify Travel.',
2596+
recommended: 'Recomendado',
2597+
},
2598+
domainPermissionInfo: {
2599+
title: 'Dominio',
2600+
restrictionPrefix: `No tienes permiso para habilitar Expensify Travel para el dominio`,
2601+
restrictionSuffix: `Tendrás que pedir a alguien de ese dominio que habilite Travel por ti.`,
2602+
accountantInvitationPrefix: `Si eres contador, considera unirte al`,
2603+
accountantInvitationLink: `programa de contadores ExpensifyApproved!`,
2604+
accountantInvitationSuffix: `para habilitar Travel para este dominio.`,
2605+
},
2606+
publicDomainError: {
2607+
title: 'Comienza con Expensify Travel',
2608+
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]).',
2609+
},
25932610
},
25942611
workspace: {
25952612
common: {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type AcceptSpotnanaTermsParams = {
2+
domain?: string;
3+
};
4+
5+
export default AcceptSpotnanaTermsParams;

src/libs/API/parameters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,4 @@ export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDel
364364
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
365365
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
366366
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
367+
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';

src/libs/API/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ type WriteCommandParameters = {
760760
[WRITE_COMMANDS.SHARE_TRACKED_EXPENSE]: Parameters.ShareTrackedExpenseParams;
761761
[WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams;
762762
[WRITE_COMMANDS.DISMISS_VIOLATION]: Parameters.DismissViolationParams;
763-
[WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null;
763+
[WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: Parameters.AcceptSpotnanaTermsParams;
764764
[WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams;
765765
[WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams;
766766
[WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams;
@@ -1042,7 +1042,6 @@ type ReadCommandParameters = {
10421042
};
10431043

10441044
const SIDE_EFFECT_REQUEST_COMMANDS = {
1045-
ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms',
10461045
AUTHENTICATE_PUSHER: 'AuthenticatePusher',
10471046
GENERATE_SPOTNANA_TOKEN: 'GenerateSpotnanaToken',
10481047
GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages',
@@ -1075,7 +1074,6 @@ type SideEffectRequestCommandParameters = {
10751074
[SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams;
10761075
[SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams;
10771076
[SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBP]: Parameters.AddPaymentCardParams;
1078-
[SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null;
10791077
[SIDE_EFFECT_REQUEST_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
10801078
[SIDE_EFFECT_REQUEST_COMMANDS.CONNECT_AS_DELEGATE]: Parameters.ConnectAsDelegateParams;
10811079
[SIDE_EFFECT_REQUEST_COMMANDS.DISCONNECT_AS_DELEGATE]: EmptyObject;

src/libs/CardUtils.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type IconAsset from '@src/types/utils/IconAsset';
1717
import localeCompare from './LocaleCompare';
1818
import {translateLocal} from './Localize';
1919
import {getDisplayNameOrDefault} from './PersonalDetailsUtils';
20-
import {getPolicy} from './PolicyUtils';
2120

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

451-
const getDescriptionForPolicyDomainCard = (domainName: string): string => {
452-
// A domain name containing a policyID indicates that this is a workspace feed
453-
const policyID = domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME)?.[1];
454-
if (policyID) {
455-
const policy = getPolicy(policyID.toUpperCase());
456-
return policy?.name ?? domainName;
457-
}
458-
return domainName;
459-
};
460-
461450
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];
462451

463452
function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry<CardFeeds>): CompanyCardFeedWithNumber {
@@ -511,7 +500,6 @@ export {
511500
getDefaultCardName,
512501
mergeCardListWithWorkspaceFeeds,
513502
isCard,
514-
getDescriptionForPolicyDomainCard,
515503
getAllCardsForWorkspace,
516504
isCardIssued,
517505
isCardHiddenFromSearch,

src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ const TravelModalStackNavigator = createModalStackNavigator<TravelNavigatorParam
114114
[SCREENS.TRAVEL.TCS]: () => require<ReactComponentModule>('../../../../pages/Travel/TravelTerms').default,
115115
[SCREENS.TRAVEL.TRIP_SUMMARY]: () => require<ReactComponentModule>('../../../../pages/Travel/TripSummaryPage').default,
116116
[SCREENS.TRAVEL.TRIP_DETAILS]: () => require<ReactComponentModule>('../../../../pages/Travel/TripDetailsPage').default,
117+
[SCREENS.TRAVEL.DOMAIN_SELECTOR]: () => require<ReactComponentModule>('../../../../pages/Travel/DomainSelectorPage').default,
118+
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: () => require<ReactComponentModule>('../../../../pages/Travel/DomainPermissionInfoPage').default,
119+
[SCREENS.TRAVEL.PUBLIC_DOMAIN_ERROR]: () => require<ReactComponentModule>('../../../../pages/Travel/PublicDomainErrorPage').default,
117120
});
118121

119122
const SplitDetailsModalStackNavigator = createModalStackNavigator<SplitDetailsNavigatorParamList>({

src/libs/Navigation/linkingConfig/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,14 +1394,17 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
13941394
[SCREENS.RIGHT_MODAL.TRAVEL]: {
13951395
screens: {
13961396
[SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS,
1397-
[SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS,
1397+
[SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS.route,
13981398
[SCREENS.TRAVEL.TRIP_SUMMARY]: ROUTES.TRAVEL_TRIP_SUMMARY.route,
13991399
[SCREENS.TRAVEL.TRIP_DETAILS]: {
14001400
path: ROUTES.TRAVEL_TRIP_DETAILS.route,
14011401
parse: {
14021402
reservationIndex: (reservationIndex: string) => parseInt(reservationIndex, 10),
14031403
},
14041404
},
1405+
[SCREENS.TRAVEL.DOMAIN_SELECTOR]: ROUTES.TRAVEL_DOMAIN_SELECTOR,
1406+
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: ROUTES.TRAVEL_DOMAIN_PERMISSION_INFO.route,
1407+
[SCREENS.TRAVEL.PUBLIC_DOMAIN_ERROR]: ROUTES.TRAVEL_PUBLIC_DOMAIN_ERROR,
14051408
},
14061409
},
14071410
[SCREENS.RIGHT_MODAL.SEARCH_REPORT]: {

src/libs/Navigation/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,12 @@ type TravelNavigatorParamList = {
15041504
reservationIndex: number;
15051505
backTo?: string;
15061506
};
1507+
[SCREENS.TRAVEL.TCS]: {
1508+
domain?: string;
1509+
};
1510+
[SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: {
1511+
domain: string;
1512+
};
15071513
};
15081514

15091515
type FullScreenNavigatorParamList = {

0 commit comments

Comments
 (0)