Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/components/EmptyStateComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function EmptyStateComponent({
buttonAction,
containerStyles,
title,
titleStyles,
subtitle,
headerStyles,
headerContentStyles,
Expand All @@ -30,7 +31,7 @@ function EmptyStateComponent({
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
const {isSmallScreenWidth} = useResponsiveLayout();
const {shouldUseNarrowLayout} = useResponsiveLayout();

const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => {
if (!event) {
Expand Down Expand Up @@ -82,7 +83,10 @@ function EmptyStateComponent({
}, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo, lottieWebViewStyles]);

return (
<ScrollView contentContainerStyle={[styles.emptyStateScrollView, {minHeight: minModalHeight}, containerStyles]}>
<ScrollView
contentContainerStyle={[{minHeight: minModalHeight}, styles.flexGrow1, styles.flexShrink0, containerStyles]}
style={styles.flex1}
>
<View style={styles.skeletonBackground}>
<SkeletonComponent
gradientOpacityEnabled
Expand All @@ -92,9 +96,9 @@ function EmptyStateComponent({
<View style={styles.emptyStateForeground}>
<View style={styles.emptyStateContent}>
<View style={[styles.emptyStateHeader(headerMediaType === CONST.EMPTY_STATE_MEDIA.ILLUSTRATION), headerStyles]}>{HeaderComponent}</View>
<View style={isSmallScreenWidth ? styles.p5 : styles.p8}>
<Text style={[styles.textAlignCenter, styles.textHeadlineH1, styles.mb2]}>{title}</Text>
<Text style={[styles.textAlignCenter, styles.textSupporting, styles.textNormal]}>{subtitle}</Text>
<View style={shouldUseNarrowLayout ? styles.p5 : styles.p8}>
<Text style={[styles.textAlignCenter, styles.textHeadlineH1, styles.mb2, titleStyles]}>{title}</Text>
{typeof subtitle === 'string' ? <Text style={[styles.textAlignCenter, styles.textSupporting, styles.textNormal]}>{subtitle}</Text> : subtitle}
{!!buttonText && !!buttonAction && (
<Button
success
Expand Down
3 changes: 2 additions & 1 deletion src/components/EmptyStateComponent/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ImageStyle} from 'expo-image';
import type {StyleProp, ViewStyle} from 'react-native';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
Expand All @@ -13,6 +13,7 @@ type MediaTypes = ValueOf<typeof CONST.EMPTY_STATE_MEDIA>;
type SharedProps<T> = {
SkeletonComponent: ValidSkeletons;
title: string;
titleStyles?: StyleProp<TextStyle>;
subtitle: string | React.ReactNode;
buttonText?: string;
buttonAction?: () => void;
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2140,6 +2140,8 @@ export default {
},
bookTravel: 'Book travel',
bookDemo: 'Book demo',
bookADemo: 'Book a demo',
toLearnMore: ' to learn more.',
termsAndConditions: {
header: 'Before we continue...',
title: 'Please read the Terms & Conditions for travel',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,8 @@ export default {
},
bookTravel: 'Reservar viajes',
bookDemo: 'Pedir demostración',
bookADemo: 'Reserva una demo',
toLearnMore: ' para obtener más información.',
termsAndConditions: {
header: 'Antes de continuar...',
title: 'Por favor, lee los Términos y condiciones para reservar viajes',
Expand Down
57 changes: 56 additions & 1 deletion src/libs/TripReservationUtils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import {Str} from 'expensify-common';
import type {Dispatch, SetStateAction} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {TravelSettings} from '@src/types/onyx';
import type {Reservation, ReservationType} from '@src/types/onyx/Transaction';
import type Transaction from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import * as Link from './actions/Link';
import Navigation from './Navigation/Navigation';

let travelSettings: OnyxEntry<TravelSettings>;
Onyx.connect({
key: ONYXKEYS.NVP_TRAVEL_SETTINGS,
callback: (val) => {
travelSettings = val;
},
});

let activePolicyID: OnyxEntry<string>;
Onyx.connect({
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (val) => {
activePolicyID = val;
},
});

let primaryLogin: string;
Onyx.connect({
key: ONYXKEYS.ACCOUNT,
callback: (val) => {
primaryLogin = val?.primaryLogin ?? '';
},
});

function getTripReservationIcon(reservationType: ReservationType): IconAsset {
switch (reservationType) {
Expand Down Expand Up @@ -38,4 +73,24 @@ function getTripEReceiptIcon(transaction?: Transaction): IconAsset | undefined {
}
}

export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon};
function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch<SetStateAction<string>>, ctaErrorMessage = ''): void {
if (Str.isSMSLogin(primaryLogin)) {
setCtaErrorMessage(translate('travel.phoneError'));
return;
}
if (isEmptyObject(travelSettings)) {
Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(activePolicyID ?? '-1', Navigation.getActiveRoute()));
return;
}
if (!travelSettings?.hasAcceptedTerms) {
Navigation.navigate(ROUTES.TRAVEL_TCS);
return;
}
if (ctaErrorMessage) {
setCtaErrorMessage('');
}
Link.openTravelDotLink(activePolicyID)?.catch(() => {
setCtaErrorMessage(translate('travel.errorMessage'));
});
}
export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon, bookATrip};
84 changes: 76 additions & 8 deletions src/pages/Search/EmptySearchView.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,103 @@
import React, {useMemo} from 'react';
import React, {useMemo, useState} from 'react';
import {Linking, View} from 'react-native';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import EmptyStateComponent from '@components/EmptyStateComponent';
import type {FeatureListItem} from '@components/FeatureList';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItem from '@components/MenuItem';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as TripsResevationUtils from '@libs/TripReservationUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';

type EmptySearchViewProps = {
type: SearchDataTypes;
};

const tripsFeatures: FeatureListItem[] = [
{
icon: Illustrations.PiggyBank,
translationKey: 'travel.features.saveMoney',
},
{
icon: Illustrations.Alert,
translationKey: 'travel.features.alerts',
},
];

function EmptySearchView({type}: EmptySearchViewProps) {
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const styles = useThemeStyles();

const [ctaErrorMessage, setCtaErrorMessage] = useState('');

const subtitleComponent = useMemo(() => {
return (
<>
<Text style={[styles.textSupporting, styles.textNormal]}>
{translate('travel.subtitle')}{' '}
<TextLink
onPress={() => {
Linking.openURL(CONST.BOOK_TRAVEL_DEMO_URL);
}}
>
{translate('travel.bookADemo')}
</TextLink>
{translate('travel.toLearnMore')}
</Text>
<View style={[styles.flex1, styles.flexRow, styles.flexWrap, styles.rowGap4, styles.pt4, styles.pl1]}>
{tripsFeatures.map((tripsFeature) => (
Comment thread
daledah marked this conversation as resolved.
<View
key={tripsFeature.translationKey}
style={styles.w100}
>
<MenuItem
title={translate(tripsFeature.translationKey)}
icon={tripsFeature.icon}
iconWidth={variables.menuIconSize}
iconHeight={variables.menuIconSize}
interactive={false}
displayInDefaultIconColor
wrapperStyle={[styles.p0, styles.cursorAuto]}
containerStyle={[styles.m0, styles.wAuto]}
numberOfLinesTitle={0}
/>
</View>
))}
</View>
{ctaErrorMessage && (
<DotIndicatorMessage
style={styles.mt1}
messages={{error: ctaErrorMessage}}
type="error"
/>
)}
</>
);
}, [styles, translate, ctaErrorMessage]);

const content = useMemo(() => {
switch (type) {
case CONST.SEARCH.DATA_TYPES.TRIP:
return {
headerMedia: LottieAnimations.TripsEmptyState,
headerStyles: [StyleUtils.getBackgroundColorStyle(theme.travelBG), styles.w100],
title: translate('search.searchResults.emptyTripResults.title'),
subtitle: translate('search.searchResults.emptyTripResults.subtitle'),
headerStyles: StyleUtils.getBackgroundColorStyle(theme.travelBG),
headerContentStyles: StyleUtils.getWidthAndHeightStyle(375, 240),
title: translate('travel.title'),
titleStyles: {...styles.textAlignLeft},
subtitle: subtitleComponent,
buttonText: translate('search.searchResults.emptyTripResults.buttonText'),
buttonAction: () => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS),
buttonAction: () => TripsResevationUtils.bookATrip(translate, setCtaErrorMessage, ctaErrorMessage),
};
case CONST.SEARCH.DATA_TYPES.CHAT:
case CONST.SEARCH.DATA_TYPES.EXPENSE:
Expand All @@ -46,7 +113,7 @@ function EmptySearchView({type}: EmptySearchViewProps) {
headerContentStyles: styles.emptyStateFolderWebStyles,
};
}
}, [type, StyleUtils, translate, theme, styles.w100, styles.emptyStateFolderWebStyles]);
}, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage]);

return (
<EmptyStateComponent
Expand All @@ -55,6 +122,7 @@ function EmptySearchView({type}: EmptySearchViewProps) {
headerMedia={content.headerMedia}
headerStyles={[content.headerStyles, styles.emptyStateCardIllustrationContainer]}
title={content.title}
titleStyles={content.titleStyles}
subtitle={content.subtitle}
buttonText={content.buttonText}
buttonAction={content.buttonAction}
Expand Down
29 changes: 2 additions & 27 deletions src/pages/Travel/ManageTrips.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {Str} from 'expensify-common';
import React, {useState} from 'react';
import {Linking, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
Expand All @@ -12,12 +11,10 @@ import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as TripsResevationUtils from '@libs/TripReservationUtils';
import colors from '@styles/theme/colors';
import * as Link from '@userActions/Link';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

const tripsFeatures: FeatureListItem[] = [
Expand All @@ -35,9 +32,7 @@ function ManageTrips() {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS);
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const policy = usePolicy(activePolicyID);

const [ctaErrorMessage, setCtaErrorMessage] = useState('');
Expand All @@ -46,9 +41,6 @@ function ManageTrips() {
return <FullScreenLoadingIndicator />;
}

const hasAcceptedTravelTerms = travelSettings?.hasAcceptedTerms;
const hasPolicyAddress = !isEmptyObject(policy?.address);

const navigateToBookTravelDemo = () => {
Linking.openURL(CONST.BOOK_TRAVEL_DEMO_URL);
};
Expand All @@ -63,24 +55,7 @@ function ManageTrips() {
ctaText={translate('travel.bookTravel')}
ctaAccessibilityLabel={translate('travel.bookTravel')}
onCtaPress={() => {
if (Str.isSMSLogin(account?.primaryLogin ?? '')) {
setCtaErrorMessage(translate('travel.phoneError'));
return;
}
if (!hasPolicyAddress) {
Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(activePolicyID ?? '-1', Navigation.getActiveRoute()));
return;
}
if (!hasAcceptedTravelTerms) {
Navigation.navigate(ROUTES.TRAVEL_TCS);
return;
}
if (ctaErrorMessage) {
setCtaErrorMessage('');
}
Link.openTravelDotLink(activePolicyID)?.catch(() => {
setCtaErrorMessage(translate('travel.errorMessage'));
});
TripsResevationUtils.bookATrip(translate, setCtaErrorMessage, ctaErrorMessage);
}}
ctaErrorMessage={ctaErrorMessage}
illustration={LottieAnimations.TripsEmptyState}
Expand Down
7 changes: 1 addition & 6 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5132,16 +5132,11 @@ const styles = (theme: ThemeColors) =>
height: '100%',
},

emptyStateScrollView: {
height: '100%',
flex: 1,
},

emptyStateForeground: {
margin: 32,
justifyContent: 'center',
alignItems: 'center',
flex: 1,
flexGrow: 1,
},

emptyStateContent: {
Expand Down