Skip to content
23 changes: 23 additions & 0 deletions src/components/BookTravelButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import {openTravelDotLink} from '@libs/actions/Link';
import {cleanupTravelProvisioningSession} from '@libs/actions/Travel';
import DateUtils from '@libs/DateUtils';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils';
Expand All @@ -15,6 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
import DotIndicatorMessage from './DotIndicatorMessage';

Expand All @@ -28,6 +30,10 @@ const navigateToAcceptTerms = (domain: string) => {
Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain));
};

// Spotnana has scheduled maintenance from February 23 at 7 AM EST (12 PM UTC) to February 24 at 12 PM EST (5 PM UTC).
const SPOTNANA_BLACKOUT_PERIOD_START = '2025-02-23T11:59:00Z';
const SPOTNANA_BLACKOUT_PERIOD_END = '2025-02-24T17:01:00Z';

function BookTravelButton({text}: BookTravelButtonProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -38,14 +44,22 @@ function BookTravelButton({text}: BookTravelButtonProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const primaryLogin = account?.primaryLogin;
const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
const [isMaintenanceModalVisible, setMaintenanceModalVisibility] = useState(false);

// 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 hideMaintenanceModal = () => setMaintenanceModalVisibility(false);

const bookATrip = useCallback(() => {
setErrorMessage('');

if (DateUtils.isCurrentTimeWithinRange(SPOTNANA_BLACKOUT_PERIOD_START, SPOTNANA_BLACKOUT_PERIOD_END)) {
setMaintenanceModalVisibility(true);
return;
}

// 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'));
Expand Down Expand Up @@ -116,6 +130,15 @@ function BookTravelButton({text}: BookTravelButtonProps) {
success
large
/>
<ConfirmModal
title={translate('travel.maintenance.title')}
onConfirm={hideMaintenanceModal}
onCancel={hideMaintenanceModal}
isVisible={isMaintenanceModalVisible}
prompt={translate('travel.maintenance.message')}
confirmText={translate('common.buttonConfirm')}
shouldShowCancelButton={false}
/>
</>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2625,6 +2625,10 @@ const translations = {
title: 'Get started with Expensify Travel',
message: `You'll need to use your work email (e.g., name@company.com) with Expensify Travel, not your personal email (e.g., name@gmail.com).`,
},
maintenance: {
title: 'Expensify Travel is getting an upgrade! 🚀',
message: `It'll be unavailable February 23-24, but back and better than ever after that. If you need help with a current trip, please call +1 866-296-7768. Thanks!`,

@ZhenjaHorbach ZhenjaHorbach Feb 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually do we need to make this text flexible so that we can pass dates in case of repeated maintenance ?

Or is this not required at the moment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used `` to not escape the ' in the It'll word 😅

Do we want to use double quotes in this case? I'm not sure what is our prefered style in this case

@ZhenjaHorbach ZhenjaHorbach Feb 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry
I was actually talking about dates 😅

And `` is good in this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, I think it would make sense if we got the dates for the blackout period from the backend, but it is OK for now given that this is a temporary feature that we either remove or improve later and show based on a specific error with dates from the backend

@ZhenjaHorbach ZhenjaHorbach Feb 13, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah
It makes sense

},
},
workspace: {
common: {
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,10 @@ const translations = {
title: 'Comienza con Expensify Travel',
message: 'Tendrás que usar tu correo electrónico laboral (por ejemplo, nombre@empresa.com) con Expensify Travel, no tu correo personal (por ejemplo, nombre@gmail.com).',
},
maintenance: {
title: '¡Expensify Travel está recibiendo una actualización! 🚀',
message: `No estará disponible del 23 al 24 de febrero, pero volverá mejor que nunca después de eso. Si necesitas ayuda con un viaje actual, por favor llama al +1 866-296-7768. ¡Gracias!`,
},
},
workspace: {
common: {
Expand Down
9 changes: 9 additions & 0 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,14 @@ function getFormattedDateRangeForPerDiem(date1: Date, date2: Date): string {
return `${format(date1, 'MMM d, yyyy')} - ${format(date2, 'MMM d, yyyy')}`;
}

/**
* Checks if the current time falls within the specified time range.
*/
const isCurrentTimeWithinRange = (startTime: string, endTime: string): boolean => {
const now = Date.now();
return isAfter(now, new Date(startTime)) && isBefore(now, new Date(endTime));
};

const DateUtils = {
isDate,
formatToDayOfWeek,
Expand Down Expand Up @@ -998,6 +1006,7 @@ const DateUtils = {
getFormattedDuration,
isFutureDay,
getFormattedDateRangeForPerDiem,
isCurrentTimeWithinRange,
};

export default DateUtils;
40 changes: 40 additions & 0 deletions tests/unit/DateUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,44 @@ describe('DateUtils', () => {
expect(DateUtils.isCardExpired(cardMonth, cardYear)).toBe(false);
});
});

describe('isCurrentTimeWithinRange', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should return true when current time is within the range', () => {
const currentTime = new Date(datetime);
jest.setSystemTime(currentTime);

const startTime = '2022-11-06T10:00:00Z';
const endTime = '2022-11-07T14:00:00Z';

expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(true);
});

it('should return false when current time is before the range', () => {
const currentTime = new Date(datetime);
jest.setSystemTime(currentTime);

const startTime = '2022-11-07T10:00:00Z';
const endTime = '2022-11-07T14:00:00Z';

expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(false);
});

it('should return false when current time is after the range', () => {
const currentTime = new Date(datetime);
jest.setSystemTime(currentTime);

const startTime = '2022-11-06T10:00:00Z';
const endTime = '2022-11-06T14:00:00Z';

expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(false);
});
});
});