Skip to content

[$250] All Single Expense Receipt Thumbnails Rendered as 1:1 Instead of Only Map Receipts (Web) #88356

Closed

Description

@m-natarajan

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 9.3.60-19
Reproducible in staging?: Y
Reproducible in production?: Y
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @shawnborton
Slack conversation (hyperlinked to channel name): #Expensify Bugs

Action Performed:

  1. Open New Expensify (Web)
  2. Submit a single expense with receipt or submit by scan
  3. Navigate to an expense report
  4. View the list of expenses with receipt thumbnails

Expected Result:

Only map receipts should use 1:1 aspect ratio
Other receipts should:
Maintain their original or intended aspect ratios

Actual Result:

All receipt thumbnails are displayed in 1:1 aspect ratio

Workaround:

Unknown

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Image Image

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @justinpersaud
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022046330890970806425
  • Upwork Job ID: 2046330890970806425
  • Last Price Increase: 2026-04-20

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Apr 20, 2026
  2. MelvinBot commented on Apr 20, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The updateAspectRatio function in Image/index.tsx:47-61 forces aspectRatio = 1 for any landscape image (width > height) when objectPosition is TOP and shouldCalculateAspectRatioForWideImage is false (the default).

    For non-map receipts, the chain works like this:

    1. ReportActionItemImage.tsx:161 sets shouldUseInitialObjectPosition: false for all non-map receipts
    2. ReceiptImage/index.tsx:252 therefore sets objectPosition=TOP
    3. Image/index.tsx sees objectPosition=TOP + landscape dimensions → forces aspectRatio = 1
    4. The resulting inline style {aspectRatio: 1, height: 'auto'} overrides any container sizing, making the thumbnail square

    Map receipts are unaffected because they render via ConfirmedRoute and never hit this code path.

    What changes do you think we should make in order to solve the problem?

    Pass shouldCalculateAspectRatioForWideImage={true} from the receipt thumbnail rendering path so that landscape receipt images preserve their real aspect ratio instead of being forced to 1:1. The most targeted place to do this is in the ReceiptImage component when rendering non-map receipt thumbnails — either by passing the prop through to ThumbnailImage/ImageWithSizeCalculation, or by wrapping the receipt thumbnail in an ImageBehaviorContextProvider with shouldSetAspectRatioInStyle={false} so the container (which already has the correct 16:9 aspect ratio) controls sizing instead of the image's inline style.

    What alternative solutions did you explore?

    • Setting objectPosition=INITIAL for non-map receipt thumbnails would also skip the updateAspectRatio logic entirely, but this changes how the image is positioned (top-aligned cropping is desirable for receipt thumbnails to show the header).
    • Changing the default of shouldCalculateAspectRatioForWideImage to true globally would fix receipts but could break other image displays that rely on the current 1:1 behavior.
    Investigation details

    Evidence chain:

    • Image/index.tsx:53-55: if (width > height && !shouldCalculateAspectRatioForWideImage) { setAspectRatio(1); return; } — this is the line that forces 1:1
    • Image/index.tsx:32-44: The aspect ratio is applied as inline style {aspectRatio, height: 'auto'} when shouldSetAspectRatioInStyle is true (the default per ImageBehaviorContextProvider)
    • ReportActionItemImage.tsx:161: shouldUseInitialObjectPosition is only true for map distance requests
    • ReceiptImage/index.tsx:229,252: objectPosition is set to TOP for all non-map receipts
    • TransactionPreviewContent.tsx:283: Outer container sets 16:9 aspect ratio for non-map receipts, but the inner Image's 1:1 inline style overrides it

    Related issues found:


    Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Apr 20, 2026
  4. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Apr 20, 2026
  5. melvin-bot commented on Apr 20, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @abdulrahuman5196 (External)

  6. melvin-bot commented on Apr 20, 2026

    @melvin-bot
  7. changed the title [-]All Single Expense Receipt Thumbnails Rendered as 1:1 Instead of Only Map Receipts (Web)[/-] [+][$250] All Single Expense Receipt Thumbnails Rendered as 1:1 Instead of Only Map Receipts (Web)[/+] on Apr 20, 2026
  8. yuvrajangadsingh commented on Apr 20, 2026

    @yuvrajangadsingh
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Single non-map receipt thumbnails in the expense preview render as 1:1 squares on web, when they should fill the 16:9 container already defined by the parent.

    What is the root cause of that problem?

    The chain goes: TransactionPreviewContent.tsx:283 renders ReportActionItemImages with shouldUseAspectRatio={!isMapDistanceRequest && !isGPSDistanceRequest} (true for all normal receipts), which sets a {aspectRatio: 16/9} container via receiptPreviewAspectRatio at src/styles/index.ts:4478. That same render sets ImageBehaviorContextProvider at ReportActionItemImages.tsx:86-89 with shouldSetAspectRatioInStyle={numberOfShownImages === 1 ? true : Str.isPDF(...)}, so single images always get shouldSetAspectRatioInStyle=true.

    Downstream, the image goes through ThumbnailImage (since shouldUseThumbnailImage=true is always passed at ReportActionItemImages.tsx:104) and then ImageWithSizeCalculation. Neither of those propagate shouldCalculateAspectRatioForWideImage, so Image/index.tsx sees it default to false. On load, updateAspectRatio at Image/index.tsx:53-55 hits the branch:

    if (width > height && !shouldCalculateAspectRatioForWideImage) {
        setAspectRatio(1);
        return;
    }

    For any landscape receipt (width > height), that sets aspectRatio=1. Because shouldSetAspectRatioInStyle=true, aspectRatioStyle resolves to {aspectRatio: 1, height: 'auto'} and gets applied directly to the <img> element, overriding the h100 class. The 16:9 container is there, but the image inside becomes a square.

    Multiple receipts are unaffected because shouldSetAspectRatioInStyle=Str.isPDF(...) for numberOfShownImages > 1, which is false for photos, so the inline style is never applied. Map receipts are unaffected because they go through ConfirmedRoute entirely, never touching Image/index.tsx.

    What changes do you think we should make in order to solve the problem?

    One line in ReportActionItemImages.tsx:88. Change:

    shouldSetAspectRatioInStyle={numberOfShownImages === 1 ? true : Str.isPDF(filename ?? '')}

    to:

    shouldSetAspectRatioInStyle={numberOfShownImages === 1 ? !shouldUseAspectRatio : Str.isPDF(filename ?? '')}

    When shouldUseAspectRatio=true (all non-map, non-GPS receipts), the parent container already declares {aspectRatio: 16/9}. The image should fill that container via object-fit: cover and object-position: top, not override it with its own inline style. Setting shouldSetAspectRatioInStyle=false in this case leaves aspectRatioStyle as {}, so the 16:9 container controls sizing, and the thumbnail shows the top of the receipt at the correct ratio.

    When shouldUseAspectRatio=false (map/GPS requests), shouldSetAspectRatioInStyle=true is preserved, same as current behavior. Map receipts render through ConfirmedRoute anyway so the context value is irrelevant for them.

    What alternative solutions did you explore? (Optional)

    Threading shouldCalculateAspectRatioForWideImage=true through ThumbnailImage and ImageWithSizeCalculation (MelvinBot's direction) would compute width/height instead of forcing 1, but would still apply the ratio as {aspectRatio: w/h, height: auto} inline on the <img>, overriding the h100 class from the container. For a portrait receipt in a 16:9 container, height: auto with aspectRatio: 0.75 would make the image taller than the container. The shouldSetAspectRatioInStyle context is the right knob because the container already owns the layout. I also considered removing objectPosition=TOP for the thumbnail path to skip updateAspectRatio entirely, but that would shift image positioning to center instead of top, which breaks the intentional behavior of showing the top of the receipt.

    working branch with the fix applied: diff

  9. abbasifaizan70 commented on Apr 20, 2026

    @abbasifaizan70
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-04-20 21:23:42 UTC.

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Single non-map receipt thumbnails in the expense preview render as 1:1 squares on web, when they should fill the 16:9 container already defined by the parent.

    What is the root cause of that problem?

    The shared Image component has a hard-coded rule that forces any wide receipt image (where width > height) to render with aspectRatio: 1 (a square) unless its parent passes shouldCalculateAspectRatioForWideImage. That prop is only forwarded by ReceiptImage when shouldUseFullHeight is true, which is only the case in a few specific contexts. For everywhere else that receipt thumbnails are rendered (report previews, transaction list, etc.), every wide receipt — photo, scan, PDF, map — falls into the setAspectRatio(1) branch and is displayed as 1:1, instead of keeping its real aspect ratio. So it isn't that "only map" gets 1:1 by design; the code unconditionally squares all wide receipts.

    const updateAspectRatio = useCallback(
    (width: number, height: number) => {
    if (!isObjectPositionTop) {
    return;
    }
    if (width > height && !shouldCalculateAspectRatioForWideImage) {
    setAspectRatio(1);
    return;
    }
    setAspectRatio(height ? width / height : 'auto');
    },
    [isObjectPositionTop, shouldCalculateAspectRatioForWideImage],
    );

    return (
    <ImageWithLoading
    onLayout={(e) => {
    if (e.nativeEvent.layout.width !== receiptImageWidth && e.timeStamp - lastUpdateWidthTimestampRef.current > MIN_UPDATE_WIDTH_DIFF) {
    setReceiptImageWidth(e.nativeEvent.layout.width);
    }
    lastUpdateWidthTimestampRef.current = e.timeStamp;
    }}
    source={typeof source === 'string' ? {uri: source} : source}
    style={[style, styles.overflowHidden]}
    isAuthTokenRequired={!!isAuthTokenRequired}
    loadingIconSize={loadingIconSize}
    loadingIndicatorStyles={loadingIndicatorStyles}
    shouldShowOfflineIndicator={false}
    objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP}
    onLoad={onLoad}
    shouldCalculateAspectRatioForWideImage={shouldUseFullHeight}
    imageWidthToCalculateHeight={receiptImageWidth}
    onError={onLoadFailure}
    resizeMode={resizeMode}
    reasonAttributes={reasonAttributes}
    />
    );

    The only place where 1:1 is actually intentional for receipts is the map distance container in ReportActionItemImage, which already applies aspectRatio: 1 to the ConfirmedRoute view explicitly:

    if (showMapAsImage) {
    return (
    <View style={[styles.w100, shouldUseFullHeight ? {aspectRatio: 1} : styles.h100]}>
    <ConfirmedRoute
    transaction={transaction}
    isSmallerIcon={!isSingleImage}
    shouldHaveBorderRadius={shouldMapHaveBorderRadius}
    interactive={false}
    requireRouteToDisplayMap
    />
    </View>
    );
    }

    What changes do you think we should make in order to solve the problem?

    const updateAspectRatio = useCallback(
    (width: number, height: number) => {
    if (!isObjectPositionTop) {
    return;
    }
    if (width > height && !shouldCalculateAspectRatioForWideImage) {
    setAspectRatio(1);
    return;
    }
    setAspectRatio(height ? width / height : 'auto');
    },
    [isObjectPositionTop, shouldCalculateAspectRatioForWideImage],
    );

    const updateAspectRatio = useCallback(
        (width: number, height: number) => {
            if (!isObjectPositionTop) {
                return;
            }
    
            setAspectRatio(height ? width / height : 'auto');
        },
        [isObjectPositionTop],
    );

    onLoad={onLoad}
    shouldCalculateAspectRatioForWideImage={shouldUseFullHeight}
    imageWidthToCalculateHeight={receiptImageWidth}

            onLoad={onLoad}
            imageWidthToCalculateHeight={receiptImageWidth}
            onError={onLoadFailure}

    shouldCalculateAspectRatioForWideImage?: boolean;

    (remove the now-unused shouldCalculateAspectRatioForWideImage?: boolean; from ImageProps)

    Demo

    Image
  10. github-actions commented on Apr 20, 2026

    @github-actions
    Contributor

    ⚠️ @abbasifaizan70 Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

  11. nabi-ebrahimi commented on Apr 21, 2026

    @nabi-ebrahimi

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Single non-map receipt thumbnails in the web expense preview are rendering as 1:1 squares, even though that preview already uses a 16:9 container. Only map receipts should appear square.

    What is the root cause of that problem?

    The bug comes from the shared Image component overriding a layout that is already controlled by the parent preview container.

    ReportActionItemImages.tsx renders single non-map receipt previews inside a 16:9 container:

    // The height varies depending on the number of images we are displaying.
    const layoutStyle = [];
    if (shouldUseAspectRatio) {
    layoutStyle.push(styles.receiptPreviewAspectRatio);
    } else if (numberOfShownImages === 1) {
    layoutStyle.push(StyleUtils.getMaximumHeight(variables.reportActionImagesSingleImageHeight), StyleUtils.getMinimumHeight(variables.reportActionImagesSingleImageHeight));
    } else if (numberOfShownImages === 2) {
    layoutStyle.push(StyleUtils.getMaximumHeight(variables.reportActionImagesDoubleImageHeight), StyleUtils.getMinimumHeight(variables.reportActionImagesDoubleImageHeight));
    } else if (numberOfShownImages > 2) {
    layoutStyle.push(StyleUtils.getMaximumHeight(variables.reportActionImagesMultipleImageHeight), StyleUtils.getMinimumHeight(variables.reportActionImagesMultipleImageHeight));
    }
    const hoverStyle = isHovered ? styles.reportPreviewBoxHoverBorder : undefined;
    const triangleWidth = variables.reportActionItemImagesMoreCornerTriangleWidth;
    return (
    <View style={styles.reportActionItemImagesContainer}>
    <View style={[styles.reportActionItemImages, hoverStyle, ...layoutStyle]}>
    {shownImages.map(({thumbnail, isThumbnail, image, isEmptyReceipt, transaction, isLocalFile, fileExtension, filename}, index) => {
    // Show a border to separate multiple images. Shown to the right for each except the last.
    const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1;
    const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {};
    const key = getMappingKey(image ?? '', index);
    return (
    <ImageBehaviorContextProvider
    key={key}
    shouldSetAspectRatioInStyle={numberOfShownImages === 1 ? true : Str.isPDF(filename ?? '')}
    >

    That 16:9 ratio comes from:

    App/src/styles/index.ts

    Lines 4492 to 4494 in 3b1058a

    receiptPreviewAspectRatio: {
    aspectRatio: 16 / 9,
    },

    ImageWithLoading.tsx passes styles.h100 to the shared Image, so the image is supposed to fill that container:

    return (
    <View
    style={[styles.w100, styles.h100, containerStyles]}
    onLayout={onLayout}
    >
    <Image
    // eslint-disable-next-line react/jsx-props-no-spreading
    {...rest}
    style={[styles.w100, styles.h100, style]}
    resizeMode={resizeMode}

    But Image/index.tsx adds its own inline style of {aspectRatio, height: 'auto'}:

    const aspectRatioStyle = useMemo(() => {
    if (!shouldSetAspectRatioInStyle || !aspectRatio) {
    return {};
    }
    if (!!imageWidthToCalculateHeight && typeof aspectRatio === 'number') {
    return {
    width: '100%',
    height: imageWidthToCalculateHeight / aspectRatio,
    };
    }
    return {aspectRatio, height: 'auto'};
    }, [shouldSetAspectRatioInStyle, aspectRatio, imageWidthToCalculateHeight]);

    Then, for top-aligned wide images, the same component forces the aspect ratio to 1:

    const updateAspectRatio = useCallback(
    (width: number, height: number) => {
    if (!isObjectPositionTop) {
    return;
    }
    if (width > height && !shouldCalculateAspectRatioForWideImage) {
    setAspectRatio(1);
    return;
    }
    setAspectRatio(height ? width / height : 'auto');
    },
    [isObjectPositionTop, shouldCalculateAspectRatioForWideImage],

    So the image stops filling the parent 16:9 preview and instead sizes itself from its own inline ratio. For wide receipts, that ratio becomes 1, which is why the thumbnail appears square.

    What changes do you think we should make in order to solve the problem?

    We should make a minimal change in the shared Image component so it no longer overrides fill-container thumbnail layouts with height: 'auto'.

    Current code in src/components/Image/index.tsx:

    const aspectRatioStyle = useMemo(() => {
    if (!shouldSetAspectRatioInStyle || !aspectRatio) {
    return {};
    }
    if (!!imageWidthToCalculateHeight && typeof aspectRatio === 'number') {
    return {
    width: '100%',
    height: imageWidthToCalculateHeight / aspectRatio,
    };
    }
    return {aspectRatio, height: 'auto'};
    }, [shouldSetAspectRatioInStyle, aspectRatio, imageWidthToCalculateHeight]);

    Replace it with:

    diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx
    @@
    -        return {aspectRatio, height: 'auto'};
    +        return {aspectRatio};

    The measured-height path just above it stays unchanged:

    if (!!imageWidthToCalculateHeight && typeof aspectRatio === 'number') {
    return {
    width: '100%',
    height: imageWidthToCalculateHeight / aspectRatio,
    };
    }

    This means we only change the normal fill-container path. The image will keep honoring styles.h100 from ImageWithLoading, so it fills the existing 16:9 preview container correctly. This fixes the issue with one small change and no new props or call-site changes.

    What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

    None

    What alternative solutions did you explore? (Optional)

    Fix demo:

    Image
  12. Yatin-Bhatti commented on Apr 21, 2026

    @Yatin-Bhatti

    Proposal

    What is the root cause of that problem?

    The issue is an Internal Style Intent Collision within the shared Image component.

    The Image component (in src/components/Image/index.tsx) contains a legacy layout heuristic called updateAspectRatio. This function is designed to "squash" landscape images into a 1:1 square whenever objectPosition is set to TOP. Because receipt thumbnails are top-aligned to preserve header visibility, they unintentionally trigger this 1:1 transform.

    The fundamental architectural flaw is that the internal aspectRatio state in the Image component is Layout-Blind—it has no awareness that a parent container (like the 16:9 wrapper in TransactionPreviewContent) has already defined an explicit aspect ratio. Currently, the component’s internal "squashing" logic unconditionally overwrites the parent's layout intent, forcing any wide receipt into a square regardless of the container's proportions.

    What changes do you think we should make in order to solve the problem?

    I propose a Declarative Style Protection strategy to ensure that internal heuristics do not clobber explicit layout definitions:

    1. Implement a Referential Guard: In src/components/Image/index.tsx, I will update the updateAspectRatio function to be Style-Aware.

    2. Detect External Ratio Intent: Before the internal logic sets aspectRatio to 1, the component will inspect the incoming style prop. I will utilize StyleSheet.flatten(style) to check if an aspectRatio has already been explicitly provided by a parent component or a theme style (such as styles.receiptPreviewAspectRatio).

    3. Heuristic Short-Circuit: If an external aspectRatio is detected in the style props, the updateAspectRatio function will NOP (No-Operation). This ensures that the component defers to the "Style Intent" of the parent container rather than applying its internal 1:1 fallback.

    4. Preserve Legacy Behavior: This fix ensures that in contexts where no ratio is defined (like a standalone image view), the 1:1 "top-crop" behavior remains intact, while specialized containers (like the 16:9 receipt preview) finally gain full control over their children’s dimensions.

    What alternative solutions did you explore? (Optional)

    I considered the "Context-Bypass" approach (suggested in other proposals) where shouldSetAspectRatioInStyle is toggled to false. However, I rejected this because it requires modifying every parent component that uses a non-square ratio. I also explored Proposal 3’s suggestion of deleting the 1:1 logic entirely, but that carries a high risk of Visual Regressions in other parts of the app that rely on square thumbnails. The proposed Style-Aware Guard is the most surgical and robust fix because it solves the conflict at the atomic component level, ensuring the Image component becomes a better "team player" in complex layouts.

  13. 4 remaining items

  14. shawnborton commented on Apr 21, 2026

    @shawnborton
    Contributor

    Hmm I have a feeling this might need to be internal... investigating in Slack for the time being.

  15. shawnborton commented on Apr 23, 2026

    @shawnborton
    Contributor

    I believe @justinpersaud will be handling this one now.

  16. justinpersaud commented on Apr 23, 2026

    @justinpersaud
    Contributor

    Yep, didn't get to it today but my plan is to work on it tomorrow

  17. justinpersaud commented on Apr 24, 2026

    @justinpersaud
    Contributor

    PR is up to stop cropping the thumbnail. I tested it in OldDot and did not see anything wrong with doign this, and asked QA to test any other flows we might have for receipt previews in the PR too.

  18. abdulrahuman5196 commented on Apr 25, 2026

    @abdulrahuman5196
    Contributor

    PR is up to stop cropping the thumbnail. I tested it in OldDot and did not see anything wrong with doign this, and asked QA to test any other flows we might have for receipt previews in the PR too.

    @justinpersaud Kindly let me know when the pr is ready for review, i could review it

  19. justinpersaud commented on Apr 25, 2026

    @justinpersaud
    Contributor

    It's a backend issue so I have assigned it internally for now

  20. melvin-bot commented on May 4, 2026

    @melvin-bot

    @justinpersaud Eep! 4 days overdue now. Issues have feelings too...

  21. justinpersaud commented on May 4, 2026

    @justinpersaud
    Contributor

    This was actually merged already.

    @shawnborton did you want to check if this is still an issue for you?

  22. shawnborton commented on May 5, 2026

    @shawnborton
    Contributor

    Yup, this is fixed on my end!

    Image

    I think we can probably close this issue since it was internal-only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsReviewingHas a PR in review

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions