diff --git a/Mobile-Expensify b/Mobile-Expensify index 9e5fc5211c4d..9e6ead35b763 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9e5fc5211c4dd2ee130aa6bb9d3f09b1728947df +Subproject commit 9e6ead35b76303685fa83ac22cad83d0955ce3d3 diff --git a/android/app/build.gradle b/android/app/build.gradle index d954287f7a1f..928d6d648d7e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009009408 - versionName "9.0.94-8" + versionCode 1009009410 + versionName "9.0.94-10" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e6c0f8051831..45abb8c07c76 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.94.8 + 9.0.94.10 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 62cb96059e63..72dd59cee48d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.94.8 + 9.0.94.10 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 10caac53aeff..613b3a038aa3 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.94 CFBundleVersion - 9.0.94.8 + 9.0.94.10 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index b976f36b5134..f2df00cbb83b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 34e38fe54979..9892aba77bd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-10", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx new file mode 100644 index 000000000000..1953acde8ad4 --- /dev/null +++ b/src/components/BookTravelButton.tsx @@ -0,0 +1,120 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useContext, useState} from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {openTravelDotLink} from '@libs/actions/Link'; +import {cleanupTravelProvisioningSession} from '@libs/actions/Travel'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import Button from './Button'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; +import DotIndicatorMessage from './DotIndicatorMessage'; + +type BookTravelButtonProps = { + text: string; +}; + +const navigateToAcceptTerms = (domain: string) => { + // Remove the previous provision session infromation if any is cached. + cleanupTravelProvisioningSession(); + Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); +}; + +function BookTravelButton({text}: BookTravelButtonProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const policy = usePolicy(activePolicyID); + const [errorMessage, setErrorMessage] = useState(''); + const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const primaryLogin = account?.primaryLogin; + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); + + // Flag indicating whether NewDot was launched exclusively for Travel, + // e.g., when the user selects "Trips" from the Expensify Classic menu in HybridApp. + const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); + + const bookATrip = useCallback(() => { + setErrorMessage(''); + + // The primary login of the user is where Spotnana sends the emails with booking confirmations, itinerary etc. It can't be a phone number. + if (!primaryLogin || Str.isSMSLogin(primaryLogin)) { + setErrorMessage(translate('travel.phoneError')); + return; + } + + // Spotnana requires an address anytime an entity is created for a policy + if (isEmptyObject(policy?.address)) { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute())); + return; + } + + const isPolicyProvisioned = policy?.travelSettings?.spotnanaCompanyID ?? policy?.travelSettings?.associatedTravelDomainAccountID; + if (policy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { + openTravelDotLink(policy?.id) + ?.then(() => { + // When a user selects "Trips" in the Expensify Classic menu, the HybridApp opens the ManageTrips page in NewDot. + // The wasNewDotLaunchedJustForTravel flag indicates if NewDot was launched solely for this purpose. + if (!NativeModules.HybridAppModule || !wasNewDotLaunchedJustForTravel) { + return; + } + + // Close NewDot if it was opened only for Travel, as its purpose is now fulfilled. + Log.info('[HybridApp] Returning to OldDot after opening TravelDot'); + NativeModules.HybridAppModule.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); + }) + ?.catch(() => { + setErrorMessage(translate('travel.errorMessage')); + }); + } else if (isPolicyProvisioned) { + navigateToAcceptTerms(CONST.TRAVEL.DEFAULT_DOMAIN); + } else { + // Determine the domain to associate with the workspace during provisioning in Spotnana. + // - If all admins share the same private domain, the workspace is tied to it automatically. + // - If admins have multiple private domains, the user must select one. + // - Public domains are not allowed; an error page is shown in that case. + const adminDomains = getAdminsPrivateEmailDomains(policy); + if (adminDomains.length === 0) { + Navigation.navigate(ROUTES.TRAVEL_PUBLIC_DOMAIN_ERROR); + } else if (adminDomains.length === 1) { + navigateToAcceptTerms(adminDomains.at(0) ?? CONST.TRAVEL.DEFAULT_DOMAIN); + } else { + Navigation.navigate(ROUTES.TRAVEL_DOMAIN_SELECTOR); + } + } + }, [policy, wasNewDotLaunchedJustForTravel, travelSettings, translate, primaryLogin, setRootStatusBarEnabled]); + + return ( + <> + {!!errorMessage && ( + + )} +