Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6aed543
add custom component for displaying enlraged receipt image
borys3kk Jun 30, 2025
59341be
create styles for ReceiptPreview
borys3kk Jul 1, 2025
04eb1b8
Merge branch 'main' into borys3kk-feature-zoom-receipt-on-hover
borys3kk Jul 1, 2025
5b3e4a6
fix review comments (remove unnecesary styles, define constants for t…
borys3kk Jul 1, 2025
df9a008
update styles and display of image in preview
borys3kk Jul 7, 2025
358eb3b
readd ReceiptAudit
borys3kk Jul 7, 2025
6eee0e7
change border, add ereceipts, add fade, add verified/issues found
borys3kk Jul 8, 2025
f5fee55
Update gradient for ReceiptHover component
Kicu Jul 8, 2025
3fa5131
Update styling for ReceiptHover component
Kicu Jul 8, 2025
875bce4
remove receiphoveraudit, update styles
borys3kk Jul 9, 2025
2b71822
fix ereceipt vertical lines
borys3kk Jul 9, 2025
e03e334
change maxHeight, remove hover in narrow layout
borys3kk Jul 10, 2025
2347038
remove props from native.ts, update variables, extract props for Rece…
borys3kk Jul 10, 2025
ea7aaf5
Add small style fixes To Receipt Preview
Kicu Jul 11, 2025
b757e38
fix distance ereceipts/refactor
borys3kk Jul 15, 2025
fa9930e
fix tests
borys3kk Jul 16, 2025
20a98b9
update proptypes for future use in receipt previews/ fix display of d…
borys3kk Jul 21, 2025
d72c556
remove console log
borys3kk Jul 21, 2025
5d0961f
fix test
borys3kk Jul 23, 2025
18f70c0
Merge branch 'main' of github.com:Expensify/App into borys3kk-feature…
borys3kk Jul 24, 2025
8e9d8c2
refactor styles, receiptPreview component, fix displaying of distance…
borys3kk Jul 25, 2025
d5d4a59
move to const
borys3kk Jul 25, 2025
818cfe7
move to const
borys3kk Jul 28, 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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@ const CONST = {
SHUTTER_SIZE: 90,
MAX_REPORT_PREVIEW_RECEIPTS: 3,
},
RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN: 120,
REPORT: {
ROLE: {
ADMIN: 'admin',
Expand Down Expand Up @@ -1503,6 +1504,8 @@ const CONST = {
SEARCH_MOST_RECENT_OPTIONS: 'search_most_recent_options',
DEBOUNCE_HANDLE_SEARCH: 'debounce_handle_search',
FAST_SEARCH_TREE_CREATION: 'fast_search_tree_creation',
SHOW_HOVER_PREVIEW_DELAY: 270,
SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 200,
},
PRIORITY_MODE: {
GSD: 'gsd',
Expand Down
7 changes: 5 additions & 2 deletions src/components/DistanceEReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import Text from './Text';
type DistanceEReceiptProps = {
/** The transaction for the distance expense */
transaction: Transaction;

/** Whether the distanceEReceipt is shown as hover preview */
hoverPreview?: boolean;
};

function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const thumbnail = hasReceipt(transaction) ? getThumbnailAndImageURIs(transaction).thumbnail : null;
Expand All @@ -42,7 +45,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
[waypoints],
);
return (
<View style={[styles.flex1, styles.alignItemsCenter]}>
<View style={[styles.flex1, styles.alignItemsCenter, hoverPreview && styles.mhv5]}>
<ScrollView
style={styles.w100}
contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]}
Expand Down
9 changes: 9 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,20 @@ type TransactionListItemType = ListItem &
/** Key used internally by React */
keyForList: string;

/** The name of the file used for a receipt */
filename?: string;

/** Attendees in the transaction */
attendees?: Attendee[];

/** Precomputed violations */
violations?: TransactionViolation[];

/** The CC for this transaction */
cardID?: number;

/** The display name of the purchaser card, if any */
cardName?: string;
};

type ReportActionListItemType = ListItem &
Expand Down
18 changes: 14 additions & 4 deletions src/components/TransactionItemRow/DataCells/ReceiptCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React from 'react';
import {View} from 'react-native';
import {Receipt} from '@components/Icon/Expensicons';
import ReceiptImage from '@components/ReceiptImage';
import ReceiptPreview from '@components/TransactionItemRow/ReceiptPreview';
import useHover from '@hooks/useHover';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -18,10 +20,10 @@ function ReceiptCell({transactionItem, isSelected}: {transactionItem: Transactio
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const backgroundStyles = isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);

const {hovered, bind} = useHover();
const isEReceipt = transactionItem.hasEReceipt && !hasReceiptSource(transactionItem);
let source = transactionItem?.receipt?.source ?? '';

if (source && typeof source === 'string') {
if (source) {
const filename = getFileName(source);
const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename);
const isReceiptPDF = Str.isPDF(filename);
Expand All @@ -36,10 +38,12 @@ function ReceiptCell({transactionItem, isSelected}: {transactionItem: Transactio
styles.overflowHidden,
backgroundStyles,
]}
onMouseEnter={bind.onMouseEnter}
onMouseLeave={bind.onMouseLeave}
>
<ReceiptImage
source={source}
isEReceipt={transactionItem.hasEReceipt && !hasReceiptSource(transactionItem)}
isEReceipt={isEReceipt}
transactionID={transactionItem.transactionID}
shouldUseThumbnailImage={!transactionItem?.receipt?.source}
isAuthTokenRequired
Expand All @@ -52,6 +56,12 @@ function ReceiptCell({transactionItem, isSelected}: {transactionItem: Transactio
loadingIndicatorStyles={styles.bgTransparent}
transactionItem={transactionItem}
/>
<ReceiptPreview
source={source}
hovered={hovered}
isEReceipt={!!isEReceipt}
transactionItem={transactionItem}
/>
</View>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function ReceiptPreview() {
return null;
}

export default ReceiptPreview;
146 changes: 146 additions & 0 deletions src/components/TransactionItemRow/ReceiptPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import type {LayoutChangeEvent} from 'react-native';
import {View} from 'react-native';
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import BaseImage from '@components/Image/BaseImage';
import type {ImageOnLoadEvent} from '@components/Image/types';
import useDebouncedState from '@hooks/useDebouncedState';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {isDistanceRequest} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {Transaction} from '@src/types/onyx';

const eReceiptAspectRatio = variables.eReceiptBGHWidth / variables.eReceiptBGHeight;

type ReceiptPreviewProps = {
/** Path to the image to be opened in the preview */
source: string;

/** Whether the preview should be shown (e.g. if we are hovered over certain ReceiptCell) */
hovered: boolean;

/** Is preview for an e-receipt */
isEReceipt: boolean;

/** Transaction object related to the preview */
transactionItem: Transaction;
};

function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: ReceiptPreviewProps) {
const isDistanceEReceipt = isDistanceRequest(transactionItem);
const styles = useThemeStyles();
const [eReceiptScaleFactor, setEReceiptScaleFactor] = useState(0);
const [imageAspectRatio, setImageAspectRatio] = useState<string | number | undefined>(undefined);
const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState<string | number | undefined>(undefined);
const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const hasMeasured = useRef(false);
const {windowHeight} = useWindowDimensions();

const handleDistanceEReceiptLayout = (e: LayoutChangeEvent) => {
if (hasMeasured.current) {
return;
}
hasMeasured.current = true;

const {height, width} = e.nativeEvent.layout;
if (height === 0) {
// on the initial layout, measured height is 0, so we want to set everything on the second one
hasMeasured.current = false;
return;
}
if (height * eReceiptScaleFactor > windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN) {
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / (windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN));
return;
}
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / height);
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
};

const updateImageAspectRatio = useCallback(
(width: number, height: number) => {
if (!source) {
return;
}

setImageAspectRatio(height ? width / height : 'auto');
},
[source],
);

const handleLoad = useCallback(
(event: ImageOnLoadEvent) => {
const {width, height} = event.nativeEvent;

updateImageAspectRatio(width, height);
},
[updateImageAspectRatio],
);

const handleEReceiptLayout = (e: LayoutChangeEvent) => {
const {width} = e.nativeEvent.layout;
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
};

useEffect(() => {
setShouldShow(hovered);
}, [hovered, setShouldShow]);

if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt)) {
return null;
}

const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt);
const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt;

return ReactDOM.createPortal(
Comment thread
borys3kk marked this conversation as resolved.
<Animated.View
entering={FadeIn.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
exiting={FadeOut.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
style={[styles.receiptPreview, styles.flexColumn, styles.alignItemsCenter, styles.justifyContentStart]}
>
{shouldShowImage ? (
<View style={[styles.w100]}>
<BaseImage
source={{uri: source}}
style={[styles.w100, {aspectRatio: imageAspectRatio}]}
onLoad={handleLoad}
/>
</View>
) : (
<View style={styles.receiptPreviewEReceiptsContainer}>
{shouldShowDistanceEReceipt ? (
<View
onLayout={handleDistanceEReceiptLayout}
style={[{transformOrigin: 'center', scale: eReceiptScaleFactor, aspectRatio: distanceEReceiptAspectRatio}]}
>
<DistanceEReceipt
transaction={transactionItem}
hoverPreview
/>
</View>
) : (
<View
onLayout={handleEReceiptLayout}
style={[styles.receiptPreviewEReceipt, {aspectRatio: eReceiptAspectRatio, scale: eReceiptScaleFactor}]}
>
<EReceipt
transactionID={transactionItem.transactionID}
transactionItem={transactionItem}
/>
</View>
)}
</View>
)}
</Animated.View>,
document.body,
);
}

export default ReceiptPreview;
6 changes: 3 additions & 3 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
];

let currentAccountID: number | undefined;
Onyx.connect({

Check warning on line 161 in src/libs/SearchUIUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (session) => {
currentAccountID = session?.accountID;
Expand Down Expand Up @@ -773,12 +773,10 @@
const report = data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];
const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
const shouldShowBlankTo = !report || isOpenExpenseReport(report);

const transactionViolations = getTransactionViolations(allViolations, transactionItem);
// Use Map.get() for faster lookups with default values
const from = personalDetailsMap.get(transactionItem.accountID.toString()) ?? emptyPersonalDetails;
const to = transactionItem.managerID && !shouldShowBlankTo ? (personalDetailsMap.get(transactionItem.managerID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails;

const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to, policy);

const transactionSection: TransactionListItemType = {
Expand All @@ -799,7 +797,7 @@
isAmountColumnWide: shouldShowAmountInWideColumn,
isTaxAmountColumnWide: shouldShowTaxAmountInWideColumn,
violations: transactionViolations,

filename: transactionItem.filename,
// Manually copying all the properties from transactionItem
transactionID: transactionItem.transactionID,
created: transactionItem.created,
Expand Down Expand Up @@ -837,6 +835,8 @@
errors: transactionItem.errors,
isActionLoading: transactionItem.isActionLoading,
hasViolation: transactionItem.hasViolation,
cardID: transactionItem.cardID,
cardName: transactionItem.cardName,
};

transactionsSections.push(transactionSection);
Expand Down
26 changes: 26 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5664,6 +5664,32 @@ const styles = (theme: ThemeColors) =>
aspectRatio: 1.7,
},

receiptPreview: {
position: 'absolute',
left: 60,
top: 60,
width: 380,
maxHeight: 'calc(100vh - 120px)',
borderRadius: variables.componentBorderRadiusLarge,
borderWidth: 1,
borderColor: theme.border,
overflow: 'hidden',
boxShadow: theme.shadow,
backgroundColor: theme.appBG,
},

receiptPreviewEReceiptsContainer: {
...sizing.w100,
...sizing.h100,
backgroundColor: colors.green800,
},

receiptPreviewEReceipt: {
...flex.flexColumn,
...flex.justifyContentCenter,
...flex.alignItemsCenter,
},

topBarWrapper: {
zIndex: 15,
},
Expand Down
9 changes: 9 additions & 0 deletions src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ type SearchTransaction = {
/** The ID of the report the transaction is associated with */
reportID: string;

/** The name of the file used for a receipt */
filename?: string;

/** The report ID of the transaction thread associated with the transaction */
transactionThreadReportID: string;

Expand All @@ -417,6 +420,12 @@ type SearchTransaction = {

/** The type of action that's pending */
pendingAction?: OnyxCommon.PendingAction;

/** The CC for this transaction */
cardID?: number;

/** The display name of the purchaser card, if any */
cardName?: string;
};

/** Model of tasks search result */
Expand Down
Loading
Loading