Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update "Hold expense" modal copy and icons #53354

Merged
merged 34 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2e4b5d
update hold expense educational modal
gijoe0295 Nov 15, 2024
aae0a6e
revert unnecessary changes
gijoe0295 Nov 15, 2024
e7be56d
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 1, 2024
3dad345
reuse FeatureTrainingModal
gijoe0295 Dec 1, 2024
0f6d33b
fix lint
gijoe0295 Dec 1, 2024
03e1c1a
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 2, 2024
6f83d98
apply illustration & update copy
gijoe0295 Dec 2, 2024
2118474
adjust bottom padding for modal text
gijoe0295 Dec 2, 2024
9e8896a
update icon size to 48x48
gijoe0295 Dec 2, 2024
6a1a89a
preseve image aspect ratio on mobile
gijoe0295 Dec 2, 2024
a7dc72f
only goBack on large screens
gijoe0295 Dec 2, 2024
d13f17b
delay hold request modal
gijoe0295 Dec 2, 2024
270d6c0
remove bottom border radius on small screens
gijoe0295 Dec 2, 2024
7c72d5a
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 10, 2024
894bee6
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 12, 2024
80975bd
fix: animation shows with image simultaneously
gijoe0295 Dec 12, 2024
dee55e1
remove hold modal on small screens as we no longer use rhp
gijoe0295 Dec 12, 2024
0a339da
make hold text color match button text color
gijoe0295 Dec 12, 2024
3c67b8f
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 24, 2024
72adbd7
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 25, 2024
dd3968e
fix: default for inexistent ids lint
gijoe0295 Dec 25, 2024
cc6a58d
fix: default for inexistent ids lint
gijoe0295 Dec 25, 2024
1f57dce
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 25, 2024
788e8c4
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Dec 30, 2024
27348dc
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Jan 7, 2025
384881a
resolve comments
gijoe0295 Jan 7, 2025
c4f55b3
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Jan 12, 2025
463041c
fix lint
gijoe0295 Jan 12, 2025
33050c4
Merge branch 'main' of https://github.com/gijoe0295/App into gijoe/52655
gijoe0295 Jan 14, 2025
ac35c61
Fix named imports lint
gijoe0295 Jan 14, 2025
c1490ef
Fix named import lint
gijoe0295 Jan 14, 2025
088eb88
make space between title & description 8px consistently on mobile & d…
gijoe0295 Jan 14, 2025
70a5ceb
fix lint
gijoe0295 Jan 14, 2025
564577c
fix lint
gijoe0295 Jan 14, 2025
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
1,207 changes: 1,207 additions & 0 deletions assets/images/product-illustrations/emptystate__holdexpense.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 70 additions & 36 deletions src/components/FeatureTrainingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import type {VideoReadyForDisplayEvent} from 'expo-av';
import type {ImageContentFit} from 'expo-image';
import React, {useCallback, useEffect, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import type {MergeExclusive} from 'type-fest';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import * as User from '@userActions/User';
import {dismissTrackTrainingModal} from '@userActions/User';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import CheckboxWithLabel from './CheckboxWithLabel';
import ImageSVG from './ImageSVG';
import Lottie from './Lottie';
import LottieAnimations from './LottieAnimations';
import type DotLottieAnimation from './LottieAnimations/types';
Expand All @@ -36,26 +41,18 @@ type VideoLoadedEventType = {

type VideoStatus = 'video' | 'animation';

type FeatureTrainingModalProps = {
/** Animation to show when video is unavailable. Useful when app is offline */
animation?: DotLottieAnimation;
type BaseFeatureTrainingModalProps = {
/** The aspect ratio to preserve for the icon, video or animation */
illustrationAspectRatio?: number;

/** Style for the inner container of the animation */
animationInnerContainerStyle?: StyleProp<ViewStyle>;
illustrationInnerContainerStyle?: StyleProp<ViewStyle>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed all these animation* to illustration* because they are no longer limited to .lottie animations only but can be images or videos as well.


/** Style for the outer container of the animation */
animationOuterContainerStyle?: StyleProp<ViewStyle>;

/** Additional styles for the animation */
animationStyle?: StyleProp<ViewStyle>;

/** URL for the video */
videoURL: string;

videoAspectRatio?: number;
illustrationOuterContainerStyle?: StyleProp<ViewStyle>;

/** Title for the modal */
title?: string;
title?: string | React.ReactNode;

/** Describe what is showing */
description?: string;
Expand All @@ -81,9 +78,6 @@ type FeatureTrainingModalProps = {
/** Link to navigate to when user wants to learn more */
onHelp?: () => void;

/** Children to render */
children?: React.ReactNode;

/** Styles for the content container */
contentInnerContainerStyles?: StyleProp<ViewStyle>;

Expand All @@ -92,15 +86,46 @@ type FeatureTrainingModalProps = {

/** Styles for the modal inner container */
modalInnerContainerStyle?: ViewStyle;

/** Children to show below title and description and above buttons */
children?: React.ReactNode;

/** Modal width */
width?: number;
};

type FeatureTrainingModalVideoProps = {
/** Animation to show when video is unavailable. Useful when app is offline */
animation?: DotLottieAnimation;

/** Additional styles for the animation */
animationStyle?: StyleProp<ViewStyle>;

/** URL for the video */
videoURL?: string;
};

type FeatureTrainingModalSVGProps = {
/** Expensicon for the page */
image: IconAsset;
Copy link
Member

Choose a reason for hiding this comment

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

Same here?

Copy link
Contributor Author

@gijoe0295 gijoe0295 Jan 7, 2025

Choose a reason for hiding this comment

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

No, we used MergeExclusive<FeatureTrainingModalVideoProps, FeatureTrainingModalSVGProps>. So if we didn't provide videoUrl or animation, we must have image. In other words, either image or videoUrl, animation.


/** Determines how the image should be resized to fit its container */
contentFitImage?: ImageContentFit;
};

// This page requires either an icon or a video/animation, but not both
type FeatureTrainingModalProps = BaseFeatureTrainingModalProps & MergeExclusive<FeatureTrainingModalVideoProps, FeatureTrainingModalSVGProps>;

function FeatureTrainingModal({
animation,
animationStyle,
animationInnerContainerStyle,
animationOuterContainerStyle,
illustrationInnerContainerStyle,
illustrationOuterContainerStyle,
videoURL,
videoAspectRatio: videoAspectRatioProp,
illustrationAspectRatio: illustrationAspectRatioProp,
image,
contentFitImage,
width = variables.onboardingModalWidth,
title = '',
description = '',
secondaryDescription = '',
Expand All @@ -116,13 +141,14 @@ function FeatureTrainingModal({
modalInnerContainerStyle,
}: FeatureTrainingModalProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
const [isModalVisible, setIsModalVisible] = useState(false);
const [willShowAgain, setWillShowAgain] = useState(true);
const [videoStatus, setVideoStatus] = useState<VideoStatus>('video');
const [isVideoStatusLocked, setIsVideoStatusLocked] = useState(false);
const [videoAspectRatio, setVideoAspectRatio] = useState(videoAspectRatioProp ?? VIDEO_ASPECT_RATIO);
const [illustrationAspectRatio, setIllustrationAspectRatio] = useState(illustrationAspectRatioProp ?? VIDEO_ASPECT_RATIO);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isOffline} = useNetwork();

Expand All @@ -149,14 +175,14 @@ function FeatureTrainingModal({
}

if ('naturalSize' in event) {
setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height);
setIllustrationAspectRatio(event.naturalSize.width / event.naturalSize.height);
} else {
setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight);
setIllustrationAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight);
}
};

const renderIllustration = useCallback(() => {
const aspectRatio = videoAspectRatio || VIDEO_ASPECT_RATIO;
const aspectRatio = illustrationAspectRatio || VIDEO_ASPECT_RATIO;

return (
<View
Expand All @@ -166,11 +192,17 @@ function FeatureTrainingModal({
// for the video until it loads. Also, when
// videoStatus === 'animation' it will
// set the same aspect ratio as the video would.
animationInnerContainerStyle,
!!videoURL && {aspectRatio},
illustrationInnerContainerStyle,
(!!videoURL || !!image) && {aspectRatio},
]}
>
{!!videoURL && videoStatus === 'video' ? (
{!!image && (
<ImageSVG
src={image}
contentFit={contentFitImage}
/>
)}
{!!videoURL && videoStatus === 'video' && (
<GestureHandlerRootView>
<VideoPlayer
url={videoURL}
Expand All @@ -182,7 +214,8 @@ function FeatureTrainingModal({
isLooping
/>
</GestureHandlerRootView>
) : (
)}
{((!videoURL && !image) || (!!videoURL && videoStatus === 'animation')) && (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The animation can show when:

  1. !!videoURL && videoStatus === 'animation': As a fallback when we're offline and thus the video cannot be downloaded; or
  2. !videoURL && !image: As the main illustration (e.g., MigratedUserWelcomeModal under onboarding/migrated-user-welcome).

<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter, !!videoURL && {aspectRatio}, animationStyle]}>
<Lottie
source={animation ?? LottieAnimations.Hands}
Expand All @@ -196,7 +229,9 @@ function FeatureTrainingModal({
</View>
);
}, [
videoAspectRatio,
image,
contentFitImage,
illustrationAspectRatio,
styles.w100,
styles.onboardingVideoPlayer,
styles.flex1,
Expand All @@ -208,14 +243,14 @@ function FeatureTrainingModal({
animationStyle,
animation,
shouldUseNarrowLayout,
animationInnerContainerStyle,
illustrationInnerContainerStyle,
]);

const toggleWillShowAgain = useCallback(() => setWillShowAgain((prevWillShowAgain) => !prevWillShowAgain), []);

const closeModal = useCallback(() => {
if (!willShowAgain) {
User.dismissTrackTrainingModal();
dismissTrackTrainingModal();
}
setIsModalVisible(false);
InteractionManager.runAfterInteractions(() => {
Expand All @@ -238,7 +273,6 @@ function FeatureTrainingModal({
onClose={closeModal}
innerContainerStyle={{
boxShadow: 'none',
borderRadius: 16,
paddingBottom: 20,
paddingTop: onboardingIsMediumOrLargerScreenWidth ? undefined : MODAL_PADDING,
...(onboardingIsMediumOrLargerScreenWidth
Expand All @@ -252,14 +286,14 @@ function FeatureTrainingModal({
...modalInnerContainerStyle,
}}
>
<View style={[styles.mh100, onboardingIsMediumOrLargerScreenWidth && styles.welcomeVideoNarrowLayout, safeAreaPaddingBottomStyle]}>
<View style={[onboardingIsMediumOrLargerScreenWidth ? {padding: MODAL_PADDING} : {paddingHorizontal: MODAL_PADDING}, animationOuterContainerStyle]}>
<View style={[styles.mh100, onboardingIsMediumOrLargerScreenWidth && StyleUtils.getWidthStyle(width), safeAreaPaddingBottomStyle]}>
<View style={[onboardingIsMediumOrLargerScreenWidth ? {padding: MODAL_PADDING} : {paddingHorizontal: MODAL_PADDING}, illustrationOuterContainerStyle]}>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

illustrationOuterContainerStyle: to suppress paddings for the images in MigratedUserWelcomeModal and hold expense education modal.

{renderIllustration()}
</View>
<View style={[styles.mt5, styles.mh5, contentOuterContainerStyles]}>
{!!title && !!description && (
<View style={[onboardingIsMediumOrLargerScreenWidth ? [styles.gap1, styles.mb8] : [styles.mb10], contentInnerContainerStyles]}>
<Text style={[styles.textHeadlineH1]}>{title}</Text>
{typeof title === 'string' ? <Text style={[styles.textHeadlineH1]}>{title}</Text> : title}
<Text style={styles.textSupporting}>{description}</Text>
{secondaryDescription.length > 0 && <Text style={[styles.textSupporting, styles.mt4]}>{secondaryDescription}</Text>}
{children}
Expand Down
Loading
Loading