Skip to content

Commit fe5b1e3

Browse files
Merge pull request #56831 from Expensify/cristi_spotnana-blackout-modal
[No QA] Modal during Spotnana scheduled maintenance
2 parents 69daa70 + f9eef24 commit fe5b1e3

File tree

5 files changed

+80
-0
lines changed

5 files changed

+80
-0
lines changed

src/components/BookTravelButton.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import usePolicy from '@hooks/usePolicy';
77
import useThemeStyles from '@hooks/useThemeStyles';
88
import {openTravelDotLink} from '@libs/actions/Link';
99
import {cleanupTravelProvisioningSession} from '@libs/actions/Travel';
10+
import DateUtils from '@libs/DateUtils';
1011
import Log from '@libs/Log';
1112
import Navigation from '@libs/Navigation/Navigation';
1213
import {getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils';
@@ -15,6 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
1516
import ROUTES from '@src/ROUTES';
1617
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1718
import Button from './Button';
19+
import ConfirmModal from './ConfirmModal';
1820
import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
1921
import DotIndicatorMessage from './DotIndicatorMessage';
2022

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

33+
// Spotnana has scheduled maintenance from February 23 at 7 AM EST (12 PM UTC) to February 24 at 12 PM EST (5 PM UTC).
34+
const SPOTNANA_BLACKOUT_PERIOD_START = '2025-02-23T11:59:00Z';
35+
const SPOTNANA_BLACKOUT_PERIOD_END = '2025-02-24T17:01:00Z';
36+
3137
function BookTravelButton({text}: BookTravelButtonProps) {
3238
const styles = useThemeStyles();
3339
const {translate} = useLocalize();
@@ -38,14 +44,22 @@ function BookTravelButton({text}: BookTravelButtonProps) {
3844
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
3945
const primaryLogin = account?.primaryLogin;
4046
const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
47+
const [isMaintenanceModalVisible, setMaintenanceModalVisibility] = useState(false);
4148

4249
// Flag indicating whether NewDot was launched exclusively for Travel,
4350
// e.g., when the user selects "Trips" from the Expensify Classic menu in HybridApp.
4451
const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
4552

53+
const hideMaintenanceModal = () => setMaintenanceModalVisibility(false);
54+
4655
const bookATrip = useCallback(() => {
4756
setErrorMessage('');
4857

58+
if (DateUtils.isCurrentTimeWithinRange(SPOTNANA_BLACKOUT_PERIOD_START, SPOTNANA_BLACKOUT_PERIOD_END)) {
59+
setMaintenanceModalVisibility(true);
60+
return;
61+
}
62+
4963
// The primary login of the user is where Spotnana sends the emails with booking confirmations, itinerary etc. It can't be a phone number.
5064
if (!primaryLogin || Str.isSMSLogin(primaryLogin)) {
5165
setErrorMessage(translate('travel.phoneError'));
@@ -116,6 +130,15 @@ function BookTravelButton({text}: BookTravelButtonProps) {
116130
success
117131
large
118132
/>
133+
<ConfirmModal
134+
title={translate('travel.maintenance.title')}
135+
onConfirm={hideMaintenanceModal}
136+
onCancel={hideMaintenanceModal}
137+
isVisible={isMaintenanceModalVisible}
138+
prompt={translate('travel.maintenance.message')}
139+
confirmText={translate('common.buttonConfirm')}
140+
shouldShowCancelButton={false}
141+
/>
119142
</>
120143
);
121144
}

src/languages/en.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2627,6 +2627,10 @@ const translations = {
26272627
title: 'Get started with Expensify Travel',
26282628
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).`,
26292629
},
2630+
maintenance: {
2631+
title: 'Expensify Travel is getting an upgrade! 🚀',
2632+
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!`,
2633+
},
26302634
},
26312635
workspace: {
26322636
common: {

src/languages/es.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,10 @@ const translations = {
26522652
title: 'Comienza con Expensify Travel',
26532653
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).',
26542654
},
2655+
maintenance: {
2656+
title: '¡Expensify Travel está recibiendo una actualización! 🚀',
2657+
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!`,
2658+
},
26552659
},
26562660
workspace: {
26572661
common: {

src/libs/DateUtils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,14 @@ function getFormattedDateRangeForPerDiem(date1: Date, date2: Date): string {
943943
return `${format(date1, 'MMM d, yyyy')} - ${format(date2, 'MMM d, yyyy')}`;
944944
}
945945

946+
/**
947+
* Checks if the current time falls within the specified time range.
948+
*/
949+
const isCurrentTimeWithinRange = (startTime: string, endTime: string): boolean => {
950+
const now = Date.now();
951+
return isAfter(now, new Date(startTime)) && isBefore(now, new Date(endTime));
952+
};
953+
946954
const DateUtils = {
947955
isDate,
948956
formatToDayOfWeek,
@@ -998,6 +1006,7 @@ const DateUtils = {
9981006
getFormattedDuration,
9991007
isFutureDay,
10001008
getFormattedDateRangeForPerDiem,
1009+
isCurrentTimeWithinRange,
10011010
};
10021011

10031012
export default DateUtils;

tests/unit/DateUtilsTest.ts

+40
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,44 @@ describe('DateUtils', () => {
280280
expect(DateUtils.isCardExpired(cardMonth, cardYear)).toBe(false);
281281
});
282282
});
283+
284+
describe('isCurrentTimeWithinRange', () => {
285+
beforeAll(() => {
286+
jest.useFakeTimers();
287+
});
288+
289+
afterAll(() => {
290+
jest.useRealTimers();
291+
});
292+
293+
it('should return true when current time is within the range', () => {
294+
const currentTime = new Date(datetime);
295+
jest.setSystemTime(currentTime);
296+
297+
const startTime = '2022-11-06T10:00:00Z';
298+
const endTime = '2022-11-07T14:00:00Z';
299+
300+
expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(true);
301+
});
302+
303+
it('should return false when current time is before the range', () => {
304+
const currentTime = new Date(datetime);
305+
jest.setSystemTime(currentTime);
306+
307+
const startTime = '2022-11-07T10:00:00Z';
308+
const endTime = '2022-11-07T14:00:00Z';
309+
310+
expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(false);
311+
});
312+
313+
it('should return false when current time is after the range', () => {
314+
const currentTime = new Date(datetime);
315+
jest.setSystemTime(currentTime);
316+
317+
const startTime = '2022-11-06T10:00:00Z';
318+
const endTime = '2022-11-06T14:00:00Z';
319+
320+
expect(DateUtils.isCurrentTimeWithinRange(startTime, endTime)).toBe(false);
321+
});
322+
});
283323
});

0 commit comments

Comments
 (0)