diff --git a/assets/images/luggage-with-lines.svg b/assets/images/luggage-with-lines.svg new file mode 100644 index 000000000000..744a1c4dc47f --- /dev/null +++ b/assets/images/luggage-with-lines.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/pending-travel.svg b/assets/images/pending-travel.svg new file mode 100644 index 000000000000..27373922b4c2 --- /dev/null +++ b/assets/images/pending-travel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/product-illustrations/emptystate__travel.svg b/assets/images/product-illustrations/emptystate__travel.svg new file mode 100644 index 000000000000..872eac3479cd --- /dev/null +++ b/assets/images/product-illustrations/emptystate__travel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__travelalerts.svg b/assets/images/simple-illustrations/simple-illustration__travelalerts.svg new file mode 100644 index 000000000000..6fb9d5253746 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__travelalerts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e2e87e10312e..16c8a4a179b5 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3104,6 +3104,7 @@ const CONST = { ARE_RULES_ENABLED: 'areRulesEnabled', ARE_PER_DIEM_RATES_ENABLED: 'arePerDiemRatesEnabled', IS_ATTENDEE_TRACKING_ENABLED: 'isAttendeeTrackingEnabled', + IS_TRAVEL_ENABLED: 'isTravelEnabled', REQUIRE_COMPANY_CARDS_ENABLED: 'requireCompanyCardsEnabled', }, DEFAULT_CATEGORIES: { @@ -7504,6 +7505,11 @@ const CONST = { TRAVEL: { DEFAULT_DOMAIN: 'domain', + STEPS: { + GET_STARTED_TRAVEL: 'GetStartedTravel', + BOOK_OR_MANAGE_YOUR_TRIP: 'BookOrManageYourTrip', + REVIEWING_REQUEST: 'ReviewingRequest', + }, PROVISIONING: { ERROR_PERMISSION_DENIED: 'permissionDenied', ERROR_ADDITIONAL_VERIFICATION_REQUIRED: 'additionalVerificationRequired', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d7da1da78f50..a96fcb3b25db 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2320,6 +2320,15 @@ const ROUTES = { return `workspaces/${policyID}/distance-rates` as const; }, }, + WORKSPACE_TRAVEL: { + route: 'workspaces/:policyID/travel', + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_TRAVEL route'); + } + return `workspaces/${policyID}/travel` as const; + }, + }, WORKSPACE_CREATE_DISTANCE_RATE: { route: 'workspaces/:policyID/distance-rates/new', getRoute: (policyID: string, transactionID?: string, reportID?: string) => @@ -2515,7 +2524,10 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getRoute: (backTo?: string) => getUrlWithBackToParam('change-workspace-educational', backTo), }, - TRAVEL_MY_TRIPS: 'travel', + TRAVEL_MY_TRIPS: { + route: 'travel', + getRoute: (policyID?: string) => `travel?${policyID ? `policyID=${policyID}` : ''}` as const, + }, TRAVEL_DOT_LINK_WEB_VIEW: { route: 'travel-dot-link', getRoute: (token: string, isTestAccount?: boolean) => `travel-dot-link?token=${token}&isTestAccount=${isTestAccount}` as const, @@ -2524,7 +2536,7 @@ const ROUTES = { route: 'travel/terms/:domain/accept', // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`travel/terms/${domain}/accept`, backTo), + getRoute: (domain: string, policyID?: string, backTo?: string) => getUrlWithBackToParam(`travel/terms/${domain}/accept?${policyID ? `policyID=${policyID}` : ''}`, backTo), }, TRAVEL_UPGRADE: { route: 'travel/upgrade', @@ -2559,7 +2571,7 @@ const ROUTES = { route: 'travel/domain-selector', // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (backTo?: string) => getUrlWithBackToParam(`travel/domain-selector`, backTo), + getRoute: (policyID?: string, backTo?: string) => getUrlWithBackToParam(`travel/domain-selector?${policyID ? `policyID=${policyID}` : ''}`, backTo), }, TRAVEL_DOMAIN_PERMISSION_INFO: { route: 'travel/domain-permission/:domain/info', @@ -2583,11 +2595,12 @@ const ROUTES = { route: 'travel/:domain/workspace-address', // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`travel/${domain}/workspace-address`, backTo), + getRoute: (domain: string, policyID?: string, backTo?: string) => getUrlWithBackToParam(`travel/${domain}/workspace-address?${policyID ? `policyID=${policyID}` : ''}`, backTo), }, TRAVEL_VERIFY_ACCOUNT: { - route: `travel/:domain/${VERIFY_ACCOUNT}`, - getRoute: (domain: string) => `travel/${domain}/${VERIFY_ACCOUNT}` as const, + route: `travel/${VERIFY_ACCOUNT}`, + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + getRoute: (domain?: string, policyID?: string, backTo?: string) => getUrlWithBackToParam(getUrlWithParams(`travel/${VERIFY_ACCOUNT}`, {domain, policyID}), backTo), }, ONBOARDING_ROOT: { route: 'onboarding', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 199e49428551..a9775e7808ef 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -687,6 +687,7 @@ const SCREENS = { OWNER_CHANGE_SUCCESS: 'Workspace_Owner_Change_Success', OWNER_CHANGE_ERROR: 'Workspace_Owner_Change_Error', DISTANCE_RATES: 'Distance_Rates', + TRAVEL: 'Travel', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', CREATE_DISTANCE_RATE_UPGRADE: 'Create_Distance_Rate_Upgrade', DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings', diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 6e14e937506b..eb664024cc97 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -20,7 +20,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; import DotIndicatorMessage from './DotIndicatorMessage'; @@ -28,36 +27,37 @@ import RenderHTML from './RenderHTML'; type BookTravelButtonProps = { text: string; + activePolicyID?: string; /** Whether to render the error message below the button */ shouldRenderErrorMessageBelowButton?: boolean; /** Function to set the shouldScrollToBottom state */ setShouldScrollToBottom?: (shouldScrollToBottom: boolean) => void; + + shouldShowVerifyAccountModal?: boolean; }; -const navigateToAcceptTerms = (domain: string, isUserValidated?: boolean) => { +const navigateToAcceptTerms = (domain: string, isUserValidated?: boolean, policyID?: string) => { // Remove the previous provision session information if any is cached. cleanupTravelProvisioningSession(); if (isUserValidated) { - Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); + Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain, policyID, Navigation.getActiveRoute())); return; } - Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain)); + Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain, policyID, Navigation.getActiveRoute())); }; -function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, setShouldScrollToBottom}: BookTravelButtonProps) { +function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, activePolicyID, setShouldScrollToBottom, shouldShowVerifyAccountModal = true}: BookTravelButtonProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const illustrations = useMemoizedLazyIllustrations(['RocketDude']); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); const phoneErrorMethodsRoute = `${environmentURL}/${ROUTES.SETTINGS_CONTACT_METHODS.getRoute(Navigation.getActiveRoute())}`; - const [activePolicyID, activePolicyIDMetadata] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const isUserValidated = account?.validated ?? false; const primaryLogin = account?.primaryLogin ?? ''; - const isLoading = isLoadingOnyxValue(activePolicyIDMetadata); const policy = usePolicy(activePolicyID); const [errorMessage, setErrorMessage] = useState(''); @@ -116,9 +116,15 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se if (policy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { openTravelDotLink(policy?.id); } else if (isPolicyProvisioned) { - navigateToAcceptTerms(CONST.TRAVEL.DEFAULT_DOMAIN); + navigateToAcceptTerms(CONST.TRAVEL.DEFAULT_DOMAIN, undefined, activePolicyID ?? undefined); } else if (!isBetaEnabled(CONST.BETAS.IS_TRAVEL_VERIFIED)) { - setVerificationModalVisibility(true); + if (!isUserValidated) { + Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(undefined, activePolicyID, Navigation.getActiveRoute())); + return; + } + if (shouldShowVerifyAccountModal) { + setVerificationModalVisibility(true); + } if (!travelSettings?.lastTravelSignupRequestTime) { requestTravelAccess(); } @@ -132,19 +138,21 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se // Always validate OTP first before proceeding to address details or terms acceptance if (!isUserValidated) { // Determine where to redirect after OTP validation - const nextStep = isEmptyObject(policy?.address) ? ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, Navigation.getActiveRoute()) : ROUTES.TRAVEL_TCS.getRoute(domain); + const nextStep = isEmptyObject(policy?.address) + ? ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, activePolicyID, Navigation.getActiveRoute()) + : ROUTES.TRAVEL_TCS.getRoute(domain, activePolicyID); setTravelProvisioningNextStep(nextStep); - Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain)); + Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain, activePolicyID, Navigation.getActiveRoute())); return; } if (isEmptyObject(policy?.address)) { // Spotnana requires an address anytime an entity is created for a policy - Navigation.navigate(ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, activePolicyID, Navigation.getActiveRoute())); } else { - navigateToAcceptTerms(domain, !!isUserValidated); + navigateToAcceptTerms(domain, !!isUserValidated, activePolicyID ?? undefined); } } else { - Navigation.navigate(ROUTES.TRAVEL_DOMAIN_SELECTOR.getRoute(Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_DOMAIN_SELECTOR.getRoute(activePolicyID, Navigation.getActiveRoute())); } }, [ primaryContactMethod, @@ -156,6 +164,8 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se translate, isUserValidated, phoneErrorMethodsRoute, + activePolicyID, + shouldShowVerifyAccountModal, ]); return ( @@ -172,8 +182,7 @@ function BookTravelButton({text, shouldRenderErrorMessageBelowButton = false, se onPress={bookATrip} accessibilityLabel={translate('travel.bookTravel')} style={styles.w100} - isLoading={isLoading} - isDisabled={!isLoading && !activePolicyID} + isDisabled={!activePolicyID} success large /> diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index de5ece484d86..2b675253cffc 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -29,6 +29,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.WORKSPACE.EXPENSIFY_CARD, SCREENS.WORKSPACE.COMPANY_CARDS, SCREENS.WORKSPACE.DISTANCE_RATES, + SCREENS.WORKSPACE.TRAVEL, SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.TROUBLESHOOT, SCREENS.SETTINGS.SAVE_THE_WORLD, diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index aec8abbfecec..8722f9d8f471 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -110,6 +110,7 @@ import InvoiceGeneric from '@assets/images/invoice-generic.svg'; import Invoice from '@assets/images/invoice.svg'; import LinkCopy from '@assets/images/link-copy.svg'; import Link from '@assets/images/link.svg'; +import LuggageWithLines from '@assets/images/luggage-with-lines.svg'; import Mail from '@assets/images/mail.svg'; import MakeAdmin from '@assets/images/make-admin.svg'; import Minus from '@assets/images/minus.svg'; @@ -358,6 +359,7 @@ export { QBOExport, SageIntacctExport, XeroExport, + LuggageWithLines, ArrowCircleClockwise, PlusCircle, }; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index bae43cfe5da7..12d387aae5df 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -4,6 +4,8 @@ import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import LaptopWithSecondScreenAndHourglass from '@assets/images/laptop-with-second-screen-and-hourglass.svg'; import LaptopWithSecondScreenSync from '@assets/images/laptop-with-second-screen-sync.svg'; import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.svg'; +import PendingTravel from '@assets/images/pending-travel.svg'; +import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; import Puzzle from '@assets/images/simple-illustrations/emptystate__puzzlepieces.svg'; import Abacus from '@assets/images/simple-illustrations/simple-illustration__abacus.svg'; import Alert from '@assets/images/simple-illustrations/simple-illustration__alert.svg'; @@ -86,6 +88,8 @@ export { Filters, TurtleInShell, Flash, + PendingTravel, + EmptyStateTravel, EmptyShelves, BlueShield, }; diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index 10a0c7fada65..ae60377ce43d 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -135,6 +135,7 @@ import LinkCopy from '@assets/images/link-copy.svg'; import Link from '@assets/images/link.svg'; import Location from '@assets/images/location.svg'; import Lock from '@assets/images/lock.svg'; +import LuggageWithLines from '@assets/images/luggage-with-lines.svg'; import Luggage from '@assets/images/luggage.svg'; import MagnifyingGlassSpyMouthClosed from '@assets/images/magnifying-glass-spy-mouth-closed.svg'; import MagnifyingGlass from '@assets/images/magnifying-glass.svg'; @@ -457,6 +458,7 @@ const Expensicons = { SageIntacctExport, XeroExport, ArrowCircleClockwise, + LuggageWithLines, }; // Create the ExpensifyIcons object from the imported Expensicons diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index f3b4206a5ef8..5dc0a626a545 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -40,6 +40,7 @@ import LaptopOnDeskWithCoffeeAndKey from '@assets/images/laptop-on-desk-with-cof import LaptopWithSecondScreenAndHourglass from '@assets/images/laptop-with-second-screen-and-hourglass.svg'; import LaptopWithSecondScreenSync from '@assets/images/laptop-with-second-screen-sync.svg'; import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.svg'; +import PendingTravel from '@assets/images/pending-travel.svg'; // Product Illustrations import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg'; import BigVault from '@assets/images/product-illustrations/big-vault.svg'; @@ -49,6 +50,7 @@ import DjBoothReferenceHands from '@assets/images/product-illustrations/dj-booth import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg'; import HoldExpense from '@assets/images/product-illustrations/emptystate__holdexpense.svg'; import ReceiptFairy from '@assets/images/product-illustrations/emptystate__receiptfairy.svg'; +import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; import Fireworks from '@assets/images/product-illustrations/fireworks.svg'; import FolderWithPapersAndWatch from '@assets/images/product-illustrations/folder-with-papers-and-watch.svg'; import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg'; @@ -151,6 +153,7 @@ import ThumbsDown from '@assets/images/simple-illustrations/simple-illustration_ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import Tire from '@assets/images/simple-illustrations/simple-illustration__tire.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; +import TravelAlerts from '@assets/images/simple-illustrations/simple-illustration__travelalerts.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__twocards-horizontal.svg'; import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg'; @@ -208,6 +211,7 @@ const Illustrations = { EmptyStateExpenses, HoldExpense, ReceiptFairy, + EmptyStateTravel, Fireworks, FolderWithPapersAndWatch, FolderWithPapers, @@ -239,6 +243,7 @@ const Illustrations = { // Other Images Hand, + PendingTravel, ReceiptUpload, RunningTurtle, Shutter, @@ -258,8 +263,11 @@ const Illustrations = { MoneyWings, MoneyIntoWallet, PerDiem, + PiggyBank, RealtimeReport, + ReceiptPartners, ReceiptWrangler, + TravelAlerts, ReportReceipt, Rules, Stopwatch, @@ -270,12 +278,10 @@ const Illustrations = { ExpensifyMobileApp, PalmTree, PaperAirplane, - PiggyBank, Pillow, Profile, QRCode, ReceiptLocationMarker, - ReceiptPartners, SendMoney, SplitBill, SubscriptionAnnual, diff --git a/src/languages/de.ts b/src/languages/de.ts index dcd1b995d9f3..5cf45d40e4a3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3636,7 +3636,7 @@ ${ subtitle: 'Nutze Expensify Travel, um die besten Reiseangebote zu erhalten und all deine Geschäftsausgaben an einem Ort zu verwalten.', features: { saveMoney: 'Spare Geld bei deinen Buchungen', - alerts: 'Erhalte Echtzeit-Updates und -Benachrichtigungen', + alerts: 'Erhalten Sie Echtzeit-Benachrichtigungen, wenn sich Ihre Reisepläne ändern', }, bookTravel: 'Reise buchen', bookDemo: 'Demo buchen', @@ -5034,6 +5034,25 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU title: 'Tagegeld', subtitle: 'Legen Sie Pauschalspesen fest, um die täglichen Ausgaben der Mitarbeitenden zu steuern.', }, + travel: { + title: 'Reisen', + subtitle: 'Buchen, verwalten und abrechnen Sie alle Ihre Geschäftsreisen.', + getStarted: { + title: 'Erste Schritte mit Expensify Travel', + subtitle: 'Wir benötigen nur noch ein paar weitere Informationen über Ihr Unternehmen, dann sind Sie bereit für den Start.', + ctaText: "Los geht's", + }, + reviewingRequest: { + title: 'Packen Sie Ihre Koffer, wir haben Ihre Anfrage...', + subtitle: 'Wir prüfen derzeit Ihre Anfrage zur Aktivierung von Expensify Travel. Keine Sorge, wir lassen Sie wissen, wenn es bereit ist.', + ctaText: 'Anfrage gesendet', + }, + bookOrManageYourTrip: { + title: 'Buchen oder verwalten Sie Ihre Reise', + subtitle: 'Nutzen Sie Expensify Travel für die besten Reiseangebote und verwalten Sie alle Ihre Geschäftsausgaben an einem Ort.', + ctaText: 'Buchen oder verwalten', + }, + }, expensifyCard: { title: 'Expensify Card', subtitle: 'Verschaffen Sie sich Einblicke in Ihre Ausgaben und behalten Sie die Kontrolle darüber.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 2a3786d2416a..d7f5135ad81a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3591,7 +3591,7 @@ const translations = { subtitle: 'Use Expensify Travel to get the best travel offers and manage all your business expenses in one place.', features: { saveMoney: 'Save money on your bookings', - alerts: 'Get realtime updates and alerts', + alerts: 'Get realtime alerts if your travel plans change', }, bookTravel: 'Book travel', bookDemo: 'Book demo', @@ -4913,6 +4913,25 @@ const translations = { title: 'Per diem', subtitle: 'Set per diem rates to control daily employee spend.', }, + travel: { + title: 'Travel', + subtitle: 'Book, manage, and reconcile all your business travel.', + getStarted: { + title: 'Get started with Expensify Travel', + subtitle: "We just need a few more pieces of info about your business, then you'll be ready for takeoff.", + ctaText: "Let's go", + }, + reviewingRequest: { + title: "Pack your bags, we've got your request...", + subtitle: "We're currently reviewing your request to enable Expensify Travel. Don't worry, we'll let you know when it's ready.", + ctaText: 'Request sent', + }, + bookOrManageYourTrip: { + title: 'Book or manage your trip', + subtitle: 'Use Expensify Travel to get the best travel offers and manage all your business expenses in a single place.', + ctaText: 'Book or manage', + }, + }, expensifyCard: { title: 'Expensify Card', subtitle: 'Gain insights and control over spend.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0a8bceea1c94..e28e6e73798b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3258,7 +3258,7 @@ ${amount} para ${merchant} - ${date}`, subtitle: 'Utiliza Expensify Travel para obtener las mejores ofertas de viaje y gestionar todos los gastos de tu negocio en un solo lugar.', features: { saveMoney: 'Ahorra dinero en tus reservas', - alerts: 'Obtén actualizaciones y alertas en tiempo real', + alerts: 'Recibe alertas en tiempo real si tus planes de viaje cambian', }, bookTravel: 'Reservar viajes', bookDemo: 'Pedir demostración', @@ -4595,6 +4595,25 @@ ${amount} para ${merchant} - ${date}`, title: 'Per diem', subtitle: 'Establece las tasas per diem para controlar los gastos diarios de los empleados.', }, + travel: { + title: 'Viajes', + subtitle: 'Reserva, gestiona y concilia todos tus viajes de negocios.', + getStarted: { + title: 'Comienza con Expensify Travel', + subtitle: 'Solo necesitamos algunos datos más sobre tu empresa y estarás listo para despegar.', + ctaText: 'Vamos allá', + }, + reviewingRequest: { + title: 'Prepara las maletas, tenemos tu solicitud...', + subtitle: 'Estamos revisando tu solicitud para habilitar Expensify Travel. No te preocupes, te avisaremos cuando esté listo.', + ctaText: 'Solicitud enviada', + }, + bookOrManageYourTrip: { + title: 'Reserva o gestiona tu viaje', + subtitle: 'Usa Expensify Travel para obtener las mejores ofertas de viaje y gestionar todos tus gastos de empresa en un solo lugar.', + ctaText: 'Reservar o gestionar', + }, + }, expensifyCard: { title: 'Tarjeta Expensify', subtitle: 'Obtén información y control sobre tus gastos.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 47cfe5f4831d..f3c3ea247266 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3643,7 +3643,7 @@ ${ subtitle: 'Utilisez Expensify Travel pour obtenir les meilleures offres de voyage et gérer toutes vos notes de frais professionnelles en un seul endroit.', features: { saveMoney: 'Économisez de l’argent sur vos réservations', - alerts: 'Recevez des mises à jour et des alertes en temps réel', + alerts: 'Recevez des alertes en temps réel si vos plans de voyage changent', }, bookTravel: 'Réserver un voyage', bookDemo: 'Réserver une démo', @@ -5040,6 +5040,25 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. title: 'Indemnité journalière', subtitle: 'Définissez des taux de per diem pour contrôler les dépenses quotidiennes des employés.', }, + travel: { + title: 'Voyage', + subtitle: "Réservez, gérez et réconciliez tous vos voyages d'affaires.", + getStarted: { + title: 'Commencer avec Expensify Travel', + subtitle: 'Nous avons juste besoin de quelques informations supplémentaires sur votre entreprise, puis vous serez prêt à décoller.', + ctaText: 'Allons-y', + }, + reviewingRequest: { + title: 'Faites vos valises, nous avons votre demande...', + subtitle: "Nous examinons actuellement votre demande d'activation d'Expensify Travel. Ne vous inquiétez pas, nous vous ferons savoir quand ce sera prêt.", + ctaText: 'Demande envoyée', + }, + bookOrManageYourTrip: { + title: 'Réservez ou gérez votre voyage', + subtitle: 'Utilisez Expensify Travel pour obtenir les meilleures offres de voyage et gérez toutes vos dépenses professionnelles en un seul endroit.', + ctaText: 'Réserver ou gérer', + }, + }, expensifyCard: { title: 'Carte Expensify', subtitle: 'Obtenez des informations et contrôlez les dépenses.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 1e8e6e7d683d..648f4f5dbe87 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3622,7 +3622,7 @@ ${ subtitle: 'Usa Expensify Travel per ottenere le migliori offerte di viaggio e gestire tutte le tue spese aziendali in un unico posto.', features: { saveMoney: 'Risparmia denaro sulle tue prenotazioni', - alerts: 'Ricevi aggiornamenti e avvisi in tempo reale', + alerts: 'Ricevi avvisi in tempo reale se i tuoi piani di viaggio cambiano', }, bookTravel: 'Prenota viaggio', bookDemo: 'Prenota demo', @@ -5017,6 +5017,25 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. title: 'Indennità di diaria', subtitle: 'Imposta le tariffe di diaria per controllare la spesa giornaliera dei dipendenti.', }, + travel: { + title: 'Viaggi', + subtitle: 'Prenota, gestisci e riconcilia tutti i tuoi viaggi di lavoro.', + getStarted: { + title: 'Inizia con Expensify Travel', + subtitle: 'Abbiamo solo bisogno di alcune informazioni aggiuntive sulla tua azienda, poi sarai pronto per partire.', + ctaText: 'Iniziamo', + }, + reviewingRequest: { + title: 'Fai le valigie, abbiamo la tua richiesta...', + subtitle: 'Stiamo attualmente esaminando la tua richiesta per abilitare Expensify Travel. Non preoccuparti, ti faremo sapere quando sarà pronto.', + ctaText: 'Richiesta inviata', + }, + bookOrManageYourTrip: { + title: 'Prenota o gestisci il tuo viaggio', + subtitle: 'Usa Expensify Travel per ottenere le migliori offerte di viaggio e gestisci tutte le tue spese aziendali in un unico posto.', + ctaText: 'Prenota o gestisci', + }, + }, expensifyCard: { title: 'Carta Expensify', subtitle: 'Ottieni informazioni e controllo sulle spese.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c9e2b130f585..b8a519366ded 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3613,7 +3613,7 @@ ${ subtitle: 'Expensify Travel を使って、最高の旅行プランを手に入れ、すべてのビジネス経費を一か所で管理しましょう。', features: { saveMoney: '予約で節約しましょう', - alerts: 'リアルタイムの更新とアラートを受け取る', + alerts: '旅行計画が変更された場合のリアルタイムアラートを受け取る', }, bookTravel: '出張を予約', bookDemo: 'デモを予約', @@ -4989,6 +4989,25 @@ _より詳しい手順については、[ヘルプサイトをご覧ください title: '日当', subtitle: '日当レートを設定して、従業員の1日あたりの支出を管理します。', }, + travel: { + title: '旅行', + subtitle: 'すべてのビジネス旅行を予約、管理、調整します。', + getStarted: { + title: 'Expensify Travelを始めましょう', + subtitle: 'お客様のビジネスについてもう少し情報が必要です。その後、準備完了です。', + ctaText: '始めましょう', + }, + reviewingRequest: { + title: '荷物をまとめてください。リクエストを受け取りました...', + subtitle: '現在、Expensify Travelの有効化リクエストを確認中です。ご心配なく、準備ができ次第お知らせします。', + ctaText: 'リクエスト送信済み', + }, + bookOrManageYourTrip: { + title: '旅行を予約または管理', + subtitle: 'Expensify Travelを使用して最高の旅行オファーを取得し、すべてのビジネス経費を一箇所で管理します。', + ctaText: '予約または管理', + }, + }, expensifyCard: { title: 'Expensify Card', subtitle: '支出を把握し、コントロールしましょう。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index df2eed9a2b28..9218f161d0af 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3620,7 +3620,7 @@ ${ subtitle: 'Gebruik Expensify Travel om de beste reisaanbiedingen te krijgen en al uw zakelijke uitgaven op één plek te beheren.', features: { saveMoney: 'Bespaar geld op je boekingen', - alerts: 'Ontvang realtime updates en meldingen', + alerts: 'Ontvang realtime meldingen als uw reisplannen veranderen', }, bookTravel: 'Reis boeken', bookDemo: 'Demo boeken', @@ -5011,6 +5011,25 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO title: 'Dagvergoeding', subtitle: 'Stel dagvergoedingen in om de dagelijkse uitgaven van medewerkers te beheren.', }, + travel: { + title: 'Reizen', + subtitle: 'Boek, beheer en verzoen al uw zakelijke reizen.', + getStarted: { + title: 'Aan de slag met Expensify Travel', + subtitle: 'We hebben nog een paar extra stukjes informatie over uw bedrijf nodig, dan bent u klaar voor vertrek.', + ctaText: 'Laten we gaan', + }, + reviewingRequest: { + title: 'Pak uw koffers, we hebben uw verzoek...', + subtitle: 'We bekijken momenteel uw verzoek om Expensify Travel in te schakelen. Maak u geen zorgen, we laten u weten wanneer het klaar is.', + ctaText: 'Verzoek verzonden', + }, + bookOrManageYourTrip: { + title: 'Boek of beheer uw reis', + subtitle: 'Gebruik Expensify Travel om de beste reisaanbiedingen te krijgen en beheer al uw zakelijke uitgaven op één plek.', + ctaText: 'Boeken of beheren', + }, + }, expensifyCard: { title: 'Expensify Card', subtitle: 'Krijg inzicht in en controle over uitgaven.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 933523a02dc0..07190e51b158 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3617,7 +3617,7 @@ ${ subtitle: 'Skorzystaj z Expensify Travel, aby uzyskać najlepsze oferty podróży i zarządzać wszystkimi wydatkami biznesowymi w jednym miejscu.', features: { saveMoney: 'Oszczędzaj pieniądze na swoich rezerwacjach', - alerts: 'Otrzymuj aktualizacje i alerty w czasie rzeczywistym', + alerts: 'Otrzymuj alerty w czasie rzeczywistym, jeśli Twoje plany podróży się zmienią', }, bookTravel: 'Zarezerwuj podróż', bookDemo: 'Zarezerwuj demo', @@ -5001,6 +5001,25 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy title: 'Dieta', subtitle: 'Ustaw stawki ryczałtu dziennego, aby kontrolować codzienne wydatki pracowników.', }, + travel: { + title: 'Podróże', + subtitle: 'Rezerwuj, zarządzaj i rozliczaj wszystkie swoje podróże służbowe.', + getStarted: { + title: 'Rozpocznij z Expensify Travel', + subtitle: 'Potrzebujemy jeszcze kilku dodatkowych informacji o Twojej firmie, a następnie będziesz gotowy do startu.', + ctaText: 'Zaczynamy', + }, + reviewingRequest: { + title: 'Spakuj walizki, mamy Twoją prośbę...', + subtitle: 'Obecnie przeglądamy Twoją prośbę o włączenie Expensify Travel. Nie martw się, damy Ci znać, gdy będzie gotowe.', + ctaText: 'Prośba wysłana', + }, + bookOrManageYourTrip: { + title: 'Zarezerwuj lub zarządzaj swoją podróżą', + subtitle: 'Użyj Expensify Travel, aby uzyskać najlepsze oferty podróży i zarządzaj wszystkimi wydatkami służbowymi w jednym miejscu.', + ctaText: 'Rezerwuj lub zarządzaj', + }, + }, expensifyCard: { title: 'Karta Expensify', subtitle: 'Uzyskaj wgląd i kontrolę nad wydatkami.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index aca1af131760..a2285156d43e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3617,7 +3617,7 @@ ${ subtitle: 'Use o Expensify Travel para obter as melhores ofertas de viagem e gerenciar todas as suas despesas de negócios em um só lugar.', features: { saveMoney: 'Economize nas suas reservas', - alerts: 'Receba atualizações e alertas em tempo real', + alerts: 'Receba alertas em tempo real se seus planos de viagem mudarem', }, bookTravel: 'Reservar viagem', bookDemo: 'Agendar demonstração', @@ -5002,6 +5002,25 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT title: 'Diária', subtitle: 'Defina diárias para controlar os gastos diários dos funcionários.', }, + travel: { + title: 'Viagem', + subtitle: 'Reserve, gerencie e reconcilie todas as suas viagens de negócios.', + getStarted: { + title: 'Comece com o Expensify Travel', + subtitle: 'Precisamos apenas de algumas informações adicionais sobre sua empresa, então você estará pronto para decolar.', + ctaText: 'Vamos lá', + }, + reviewingRequest: { + title: 'Faça as malas, temos sua solicitação...', + subtitle: 'Estamos atualmente revisando sua solicitação para habilitar o Expensify Travel. Não se preocupe, avisaremos quando estiver pronto.', + ctaText: 'Solicitação enviada', + }, + bookOrManageYourTrip: { + title: 'Reserve ou gerencie sua viagem', + subtitle: 'Use o Expensify Travel para obter as melhores ofertas de viagem e gerencie todas as suas despesas comerciais em um só lugar.', + ctaText: 'Reservar ou gerenciar', + }, + }, expensifyCard: { title: 'Cartão Expensify', subtitle: 'Obtenha insights e controle sobre os gastos.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8d8abe17ffcd..d63cee2da410 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3567,7 +3567,7 @@ ${ subtitle: '使用 Expensify Travel 获取最优惠的旅行方案,并在一个地方管理您所有的商务报销。', features: { saveMoney: '在预订时省钱', - alerts: '获取实时更新和提醒', + alerts: '如果您的旅行计划发生变化,获取实时提醒', }, bookTravel: '预订出行', bookDemo: '预订演示', @@ -4915,6 +4915,25 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM title: '每日津贴', subtitle: '设置每日津贴标准以控制员工日常支出。', }, + travel: { + title: '旅行', + subtitle: '预订、管理和核对您的所有商务旅行。', + getStarted: { + title: '开始使用 Expensify Travel', + subtitle: '我们只需要您企业的更多信息,然后您就可以准备出发了。', + ctaText: '开始吧', + }, + reviewingRequest: { + title: '收拾行李,我们已收到您的请求...', + subtitle: '我们正在审查您启用 Expensify Travel 的请求。别担心,准备就绪时我们会通知您。', + ctaText: '请求已发送', + }, + bookOrManageYourTrip: { + title: '预订或管理您的旅行', + subtitle: '使用 Expensify Travel 获得最佳旅行优惠,并在一个地方管理所有商务费用。', + ctaText: '预订或管理', + }, + }, expensifyCard: { title: 'Expensify Card', subtitle: '获取支出洞察并实现有效管控。', diff --git a/src/libs/API/parameters/AcceptSpotnanaTermsParams.ts b/src/libs/API/parameters/AcceptSpotnanaTermsParams.ts index a5a67e76eac3..f854601beffb 100644 --- a/src/libs/API/parameters/AcceptSpotnanaTermsParams.ts +++ b/src/libs/API/parameters/AcceptSpotnanaTermsParams.ts @@ -1,5 +1,6 @@ type AcceptSpotnanaTermsParams = { domain?: string; + policyID?: string; }; export default AcceptSpotnanaTermsParams; diff --git a/src/libs/API/parameters/EnablePolicyTravelParams.ts b/src/libs/API/parameters/EnablePolicyTravelParams.ts new file mode 100644 index 000000000000..4fa14a661739 --- /dev/null +++ b/src/libs/API/parameters/EnablePolicyTravelParams.ts @@ -0,0 +1,6 @@ +type EnablePolicyTravelParams = { + policyID: string; + enabled: boolean; +}; + +export default EnablePolicyTravelParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f27feccfaf62..6ed2ca4cf6d6 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -219,6 +219,7 @@ export type {default as EnablePolicyCategoriesParams} from './EnablePolicyCatego export type {default as EnablePolicyConnectionsParams} from './EnablePolicyConnectionsParams'; export type {default as TogglePolicyReceiptPartnersParams} from './TogglePolicyReceiptPartnersParams'; export type {default as EnablePolicyDistanceRatesParams} from './EnablePolicyDistanceRatesParams'; +export type {default as EnablePolicyTravelParams} from './EnablePolicyTravelParams'; export type {default as EnablePolicyTagsParams} from './EnablePolicyTagsParams'; export type {default as SetPolicyTagsEnabled} from './SetPolicyTagsEnabled'; export type {default as EnablePolicyWorkflowsParams} from './EnablePolicyWorkflowsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2da2814ba411..d0b8c52903c0 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -242,6 +242,7 @@ const WRITE_COMMANDS = { TOGGLE_RECEIPT_PARTNERS: 'TogglePolicyReceiptPartners', INVITE_WORKSPACE_EMPLOYEES_TO_UBER: 'InviteWorkspaceEmployeesToUber', ENABLE_POLICY_DISTANCE_RATES: 'EnablePolicyDistanceRates', + ENABLE_POLICY_TRAVEL: 'EnablePolicyTravel', ENABLE_POLICY_TAGS: 'EnablePolicyTags', ENABLE_POLICY_TAXES: 'EnablePolicyTaxes', ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows', @@ -773,6 +774,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.TOGGLE_RECEIPT_PARTNERS]: Parameters.TogglePolicyReceiptPartnersParams; [WRITE_COMMANDS.INVITE_WORKSPACE_EMPLOYEES_TO_UBER]: Parameters.InviteWorkspaceEmployeesToUberParams; [WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES]: Parameters.EnablePolicyDistanceRatesParams; + [WRITE_COMMANDS.ENABLE_POLICY_TRAVEL]: Parameters.EnablePolicyTravelParams; [WRITE_COMMANDS.ENABLE_POLICY_TAGS]: Parameters.EnablePolicyTagsParams; [WRITE_COMMANDS.ENABLE_POLICY_TAXES]: Parameters.EnablePolicyTaxesParams; [WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams; diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index d809e0f1a855..99a11121f25b 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -32,6 +32,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default, [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, + [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, } satisfies Screens; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 21a93c3a6920..e0f4b1a230df 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -222,6 +222,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { }, [SCREENS.RIGHT_MODAL.TRAVEL]: { screens: { - [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, + [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS.route, [SCREENS.TRAVEL.TRAVEL_DOT_LINK_WEB_VIEW]: ROUTES.TRAVEL_DOT_LINK_WEB_VIEW.route, [SCREENS.TRAVEL.UPGRADE]: ROUTES.TRAVEL_UPGRADE.route, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS.route, @@ -1929,6 +1929,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.DISTANCE_RATES]: { path: ROUTES.WORKSPACE_DISTANCE_RATES.route, }, + [SCREENS.WORKSPACE.TRAVEL]: { + path: ROUTES.WORKSPACE_TRAVEL.route, + }, [SCREENS.WORKSPACE.RULES]: { path: ROUTES.WORKSPACE_RULES.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 2974734b708c..cb9bd3c4a467 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2198,7 +2198,9 @@ type RightModalNavigatorParamList = { }; type TravelNavigatorParamList = { - [SCREENS.TRAVEL.MY_TRIPS]: undefined; + [SCREENS.TRAVEL.MY_TRIPS]: { + policyID: string; + }; [SCREENS.TRAVEL.TRAVEL_DOT_LINK_WEB_VIEW]: { token: string; isTestAccount?: string; @@ -2219,6 +2221,7 @@ type TravelNavigatorParamList = { }; [SCREENS.TRAVEL.TCS]: { domain?: string; + policyID?: string; }; [SCREENS.TRAVEL.DOMAIN_PERMISSION_INFO]: { domain: string; @@ -2231,6 +2234,7 @@ type TravelNavigatorParamList = { domain: string; // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; + policyID?: string; }; [SCREENS.TRAVEL.PUBLIC_DOMAIN_ERROR]: { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md @@ -2241,11 +2245,15 @@ type TravelNavigatorParamList = { backTo?: Routes; }; [SCREENS.TRAVEL.DOMAIN_SELECTOR]: { + policyID?: string; // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; [SCREENS.TRAVEL.VERIFY_ACCOUNT]: { - domain: string; + domain?: string; + policyID?: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; }; }; @@ -2398,6 +2406,9 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.DISTANCE_RATES]: { policyID: string; }; + [SCREENS.WORKSPACE.TRAVEL]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: { policyID: string; }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 13374a6ac360..a6518d2bfc1c 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; -import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, Report, TaxRate, Transaction} from '@src/types/onyx'; +import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, Report, TaxRate, Transaction, TravelSettings} from '@src/types/onyx'; import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon'; import type { ConnectionLastSync, @@ -43,6 +43,8 @@ import {isPublicDomain} from './ValidationUtils'; type MemberEmailsToAccountIDs = Record; +type TravelStep = ValueOf; + type WorkspaceDetails = { policyID: string | undefined; name: string; @@ -1588,6 +1590,32 @@ function isMemberPolicyAdmin(policy: OnyxEntry, memberEmail: string | un return admins.some((admin) => admin.email === memberEmail); } +/** + * Determines which travel step should be shown based on policy state + */ +function getTravelStep( + policy: OnyxEntry, + travelSettings: TravelSettings | undefined, + isTravelVerifiedBetaEnabled: boolean, + policies: OnyxCollection, + currentUserLogin: string | undefined, +): TravelStep { + const adminDomains = getAdminsPrivateEmailDomains(policy); + const activePolicies = getActivePolicies(policies, currentUserLogin); + const groupPaidPolicies = activePolicies.filter(isPaidGroupPolicy); + + if (adminDomains.length === 0 || groupPaidPolicies.length < 1 || !isPaidGroupPolicy(policy)) { + return CONST.TRAVEL.STEPS.GET_STARTED_TRAVEL; + } + if (policy?.travelSettings?.hasAcceptedTerms) { + return CONST.TRAVEL.STEPS.BOOK_OR_MANAGE_YOUR_TRIP; + } + if (!isTravelVerifiedBetaEnabled && travelSettings?.lastTravelSignupRequestTime) { + return CONST.TRAVEL.STEPS.REVIEWING_REQUEST; + } + return CONST.TRAVEL.STEPS.GET_STARTED_TRAVEL; +} + export { canEditTaxRate, escapeTagName, @@ -1745,6 +1773,7 @@ export { getPolicyEmployeeAccountIDs, isMemberPolicyAdmin, getActivePoliciesWithExpenseChatAndPerDiemEnabled, + getTravelStep, getActivePoliciesWithExpenseChatAndPerDiemEnabledAndHasRates, isDefaultTagName, }; diff --git a/src/libs/actions/Policy/Travel.ts b/src/libs/actions/Policy/Travel.ts new file mode 100644 index 000000000000..35209bfe63d5 --- /dev/null +++ b/src/libs/actions/Policy/Travel.ts @@ -0,0 +1,60 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {EnablePolicyTravelParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {goBackWhenEnableFeature} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxData} from '@src/types/onyx/Request'; + +function enablePolicyTravel(policyID: string, enabled: boolean) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + isTravelEnabled: enabled, + pendingFields: { + isTravelEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + isTravelEnabled: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + isTravelEnabled: !enabled, + pendingFields: { + isTravelEnabled: null, + }, + }, + }, + ], + }; + + const parameters: EnablePolicyTravelParams = {policyID, enabled}; + + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TRAVEL, parameters, onyxData); + + if (enabled && getIsNarrowLayout()) { + goBackWhenEnableFeature(policyID); + } +} + +// eslint-disable-next-line import/prefer-default-export +export {enablePolicyTravel}; diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 7cf4c2021d55..93397955f16a 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -34,6 +34,7 @@ const enablePolicyFeatureCommand = [ WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, WRITE_COMMANDS.ENABLE_POLICY_INVOICING, + WRITE_COMMANDS.ENABLE_POLICY_TRAVEL, ] as const; type EnablePolicyFeatureCommand = TupleToUnion; diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 7f6439ea9b0e..10740c069c24 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -10,7 +10,7 @@ import type {Route} from '@src/ROUTES'; /** * Accept Spotnana terms and conditions to receive a proper token used for authenticating further actions */ -function acceptSpotnanaTerms(domain?: string) { +function acceptSpotnanaTerms(domain?: string, policyID?: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: 'merge', @@ -37,6 +37,15 @@ function acceptSpotnanaTerms(domain?: string) { isLoading: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + travelSettings: { + hasAcceptedTerms: true, + }, + }, + }, ]; const failureData: OnyxUpdate[] = [ @@ -50,7 +59,7 @@ function acceptSpotnanaTerms(domain?: string) { }, ]; - const params: AcceptSpotnanaTermsParams = {domain}; + const params: AcceptSpotnanaTermsParams = {domain, policyID}; // We need to call this API immediately to get the response and open the travel page. // See https://github.com/Expensify/App/pull/69769#discussion_r2368967354 for more info. diff --git a/src/libs/openTravelDotLink/index.native.ts b/src/libs/openTravelDotLink/index.native.ts index 048270bfc5f7..09550dda35b1 100644 --- a/src/libs/openTravelDotLink/index.native.ts +++ b/src/libs/openTravelDotLink/index.native.ts @@ -9,11 +9,11 @@ const openTravelDotLink = (activePolicyID?: string) => { Navigation.navigate(ROUTES.TRAVEL_DOT_LINK_WEB_VIEW.getRoute(response.spotnanaToken, response.isTestAccount)); return; } - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicyID)); }) ?.catch((error) => { console.error('Failed to get travel dot link:', error); - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicyID)); }); }; diff --git a/src/libs/openTravelDotLink/index.ts b/src/libs/openTravelDotLink/index.ts index 7e0c7055818f..4b14c5c7b794 100644 --- a/src/libs/openTravelDotLink/index.ts +++ b/src/libs/openTravelDotLink/index.ts @@ -6,7 +6,7 @@ const openTravelDotLink = (activePolicyID?: string) => { openTravelDotLinkWeb(activePolicyID) ?.then(() => {}) ?.catch(() => { - Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS); + Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(activePolicyID)); }); }; diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index a15a16bec888..d25e23d6e2e6 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -152,7 +152,8 @@ function EmptySearchViewContent({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['PiggyBank', 'Alert']); + + const illustrations = useMemoizedLazyIllustrations(['PiggyBank', 'TravelAlerts']); const tripsFeatures: FeatureListItem[] = useMemo( () => [ @@ -161,11 +162,11 @@ function EmptySearchViewContent({ translationKey: 'travel.features.saveMoney', }, { - icon: illustrations.Alert, + icon: illustrations.TravelAlerts, translationKey: 'travel.features.alerts', }, ], - [illustrations.PiggyBank, illustrations.Alert], + [illustrations.PiggyBank, illustrations.TravelAlerts], ); const [contextMenuAnchor, setContextMenuAnchor] = useState(null); const handleContextMenuAnchorRef = useCallback((node: RNText | null) => { @@ -306,11 +307,14 @@ function EmptySearchViewContent({ ))} - + ); - }, [contextMenuAnchor, handleContextMenuAnchorRef, styles, translate, tripsFeatures]); + }, [contextMenuAnchor, handleContextMenuAnchorRef, styles, translate, activePolicy?.id, tripsFeatures]); // Default 'Folder' lottie animation, along with its background styles const defaultViewItemHeader = useSearchEmptyStateIllustration(); diff --git a/src/pages/Travel/DomainSelectorPage.tsx b/src/pages/Travel/DomainSelectorPage.tsx index 27dc0eaf8beb..2dbf52beac9b 100644 --- a/src/pages/Travel/DomainSelectorPage.tsx +++ b/src/pages/Travel/DomainSelectorPage.tsx @@ -17,6 +17,7 @@ import {cleanupTravelProvisioningSession, setTravelProvisioningNextStep} from '@ import Navigation from '@libs/Navigation/Navigation'; import type {TravelNavigatorParamList} from '@libs/Navigation/types'; import {getAdminsPrivateEmailDomains, getMostFrequentEmailDomain} from '@libs/PolicyUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,9 +35,10 @@ function DomainSelectorPage({route}: DomainSelectorPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const policy = usePolicy(activePolicyID); + const {policyID} = route.params; + const policy = usePolicy(policyID); const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true}); + const [selectedDomain, setSelectedDomain] = useState(); const domains = useMemo(() => getAdminsPrivateEmailDomains(policy), [policy]); @@ -59,50 +61,54 @@ function DomainSelectorPage({route}: DomainSelectorPageProps) { // Always validate OTP first before proceeding to address details or terms acceptance if (!isUserValidated) { // Determine where to redirect after OTP validation - const nextStep = isEmptyObject(policy?.address) ? ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, Navigation.getActiveRoute()) : ROUTES.TRAVEL_TCS.getRoute(domain); + const nextStep = isEmptyObject(policy?.address) + ? ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, policyID, Navigation.getActiveRoute()) + : ROUTES.TRAVEL_TCS.getRoute(domain, policyID); setTravelProvisioningNextStep(nextStep); - Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain)); + Navigation.navigate(ROUTES.TRAVEL_VERIFY_ACCOUNT.getRoute(domain, policyID)); return; } if (isEmptyObject(policy?.address)) { // Spotnana requires an address anytime an entity is created for a policy - Navigation.navigate(ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_WORKSPACE_ADDRESS.getRoute(domain, policyID, Navigation.getActiveRoute())); } else { cleanupTravelProvisioningSession(); - Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); + Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain, policyID)); } }; return ( - - Navigation.goBack(route.params.backTo)} - /> - {translate('travel.domainSelector.subtitle')} - - {translate('travel.domainSelector.title')} - - setSelectedDomain(option.value)} - data={data} - ListItem={TravelDomainListItem} - shouldShowTooltips - footerContent={ -