Skip to content

[Due for payment 2025-11-07] [Due for payment 2025-10-30] [$250] Expense - Scan expense with video as receipt can be created by dropping video to Scan zone #72402

Closed

Description

@lanitochka17

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.2.29-1
Reproducible in staging?: Y
Reproducible in production?: Y
If this was caught during regression testing, add the test name, ID and link from TestRail: https://test-management.browserstack.com/projects/2219752/test-runs/TR-1237/41237997/945417200?q=drag&issue_type=jira
Email or phone of affected tester (no customers): jsdoinjsoijsiodj@gmail.com
Issue reported by: Applause - Internal Team

Action Performed:

  1. Go to staging.new.expensify.com
  2. Go to workspace chat
  3. Click + > Create expense > Scan
  4. Drag and drop a video to the Scan RHP
    → Invalid file type error shows up - Expected behavior
  5. Close the RHP
  6. Drag and drop a video to Scan zone
  7. Click Create expense
  8. Open the expense thread

Expected Result:

"Invalid file type" error will show up when dropping a video to Scan zone in workspace chat

Actual Result:

User can create a scan expense with video as receipt by dropping a video to Scan zone in workspace chat
There is no "Invalid file type" error
After creating the expense, error shows up in transaction thread

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
  • MacOS: Desktop
Platforms Tested: On which of our officially supported platforms was this issue tested:
  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Add any screenshot/video evidence
Bug6972307_1760211709021.video_receipt.mp4

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021977603641386532131
  • Upwork Job ID: 1977603641386532131
  • Last Price Increase: 2025-10-27
Issue OwnerCurrent Issue Owner: @kadiealexander

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 12, 2025
  2. melvin-bot commented on Oct 12, 2025

    @melvin-bot

    Triggered auto assignment to @kadiealexander (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

  3. TaduJR commented on Oct 12, 2025

    @TaduJR
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2025-10-13 07:13:05 UTC.

    Proposal

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

    When a user drags and drops a video file to the Scan zone in a workspace chat, the video is accepted and can be used to create a scan expense. However, videos are not allowed as receipt types. The expected behavior is that an "Invalid file type" error should appear when dropping a video to the Scan zone, similar to what happens when dropping a video in the Scan RHP (Right Hand Panel).

    What is the root cause of that problem?

    The root cause is that the useFilesValidation hook is initialized with the wrong isValidatingReceipts parameter value in useAttachmentUploadValidation.

    The issue occurs in this file:

    const {validateFiles, PDFValidationComponent, ErrorModal} = useFilesValidation(onFilesValidated, false);

    The hook is initialized with isValidatingReceipts = false:

    const {validateFiles, PDFValidationComponent, ErrorModal} = useFilesValidation(onFilesValidated, false);

    This single hook instance is then used for both:

    1. Attachment drops (correct - should use false)
    2. Receipt/Scan zone drops (WRONG - should use true)

    Why this causes the bug:

    When isValidatingReceipts = false is passed to useFilesValidation, the validation flow becomes:

    1. validateFiles is called with the video file
    2. Internally calls isValidFile:

    1. Which calls validateAttachment with isValidatingReceipts = false:

    const validateAttachment = (file: FileObject, isCheckingMultipleFiles?: boolean, isValidatingReceipt?: boolean) => {
    const maxFileSize = isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
    if (isValidatingReceipt && !isValidReceiptExtension(file)) {
    return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE;
    }
    if (!Str.isImage(file.name ?? '') && !hasHeicOrHeifExtension(file) && (file?.size ?? 0) > maxFileSize) {
    return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE;
    }
    if (isValidatingReceipt && (file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
    return CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL;
    }
    return '';
    };

    1. The critical check at line 553 is SKIPPED because isValidatingReceipt is false:

    if (isValidatingReceipt && !isValidReceiptExtension(file)) {
    return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE;
    }

    if (isValidatingReceipt && !isValidReceiptExtension(file)) {
        return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE;
    }

    Since the check is skipped, video files pass validation and are allowed to create expenses.

    In contrast, the Scan RHP works correctly:

    const {validateFiles, PDFValidationComponent, ErrorModal} = useFilesValidation(setReceiptFilesAndNavigate);

    The hook is called with isValidatingReceipts = true (the default), so videos are properly rejected.

    Video extensions are NOT in the allowed list:

    ALLOWED_RECEIPT_EXTENSIONS: ['heif', 'heic', 'jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'],

    The ALLOWED_RECEIPT_EXTENSIONS array does not include video formats like 'mp4', 'mov', 'avi', etc.

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

    We need to use two separate instances of the useFilesValidation hook:

    1. One for attachments with isValidatingReceipts = false
    2. One for receipts with isValidatingReceipts = true

    This follows the hook's intended design pattern where isValidatingReceipts is a configuration parameter set at initialization, not a runtime parameter.

    Changes needed in:

    https://github.com/Expensify/App/blob/89c45c98a802b6d039a6d36d7afc761083e89212/src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts

    Step 1: Add useMemo to imports (line 1):

    import {useCallback, useContext, useMemo, useRef} from 'react';

    Step 2: Split the unified onFilesValidated callback into two separate callbacks (replace lines 70-117):

    const onAttachmentFilesValidated = useCallback(
        (files: FileObject[], dataTransferItems: DataTransferItem[]) => {
            showAttachmentModalScreen(files, dataTransferItems);
        },
        [showAttachmentModalScreen],
    );
    
    const onReceiptFilesValidated = useCallback(
        (files: FileObject[], dataTransferItems: DataTransferItem[]) => {
            if (files.length === 0) {
                return;
            }
    
            if (shouldAddOrReplaceReceipt && transactionID) {
                const source = URL.createObjectURL(files.at(0) as Blob);
                replaceReceipt({transactionID, file: files.at(0) as File, source});
                return;
            }
    
            const initialTransaction = initMoneyRequest({
                reportID,
                newIouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
                report,
                parentReport: newParentReport,
                currentDate,
            });
    
            files.forEach((file, index) => {
                const source = URL.createObjectURL(file as Blob);
                const newTransaction =
                    index === 0
                        ? (initialTransaction as Partial<OnyxTypes.Transaction>)
                        : buildOptimisticTransactionAndCreateDraft({
                              initialTransaction: initialTransaction as Partial<OnyxTypes.Transaction>,
                              currentUserPersonalDetails,
                              reportID,
                          });
                const newTransactionID = newTransaction?.transactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
                setMoneyRequestReceipt(newTransactionID, source, file.name ?? '', true);
                setMoneyRequestParticipantsFromReport(newTransactionID, report);
            });
            Navigation.navigate(
                ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(
                    CONST.IOU.ACTION.CREATE,
                    isSelfDM(report) ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT,
                    CONST.IOU.OPTIMISTIC_TRANSACTION_ID,
                    reportID,
                ),
            );
        },
        [currentDate, currentUserPersonalDetails, newParentReport, report, reportID, shouldAddOrReplaceReceipt, transactionID],
    );

    Step 3: Replace the single hook instance with two separate instances (replace line 119):

    // Separate validation hooks for attachments (isValidatingReceipts=false) and receipts (isValidatingReceipts=true)
    const {validateFiles: validateAttachmentFiles, PDFValidationComponent: AttachmentPDFValidationComponent, ErrorModal: AttachmentErrorModal} = useFilesValidation(
        onAttachmentFilesValidated,
        false,
    );
    const {validateFiles: validateReceiptFiles, PDFValidationComponent: ReceiptPDFValidationComponent, ErrorModal: ReceiptErrorModal} = useFilesValidation(onReceiptFilesValidated, true);

    Step 4: Update validateAttachments to use the attachment-specific validation (line 164):

    Replace:

    attachmentUploadType.current = 'attachment';
    validateFiles(fileObjects, filteredItems);

    With:

    validateAttachmentFiles(fileObjects, filteredItems);

    And update the dependency array (line 166):

    [isAttachmentPreviewActive, validateAttachmentFiles],

    Step 5: Update onReceiptDropped to use the receipt-specific validation (replace lines 179-191):

    Replace:

    const files = getFilesFromClipboardEvent(e);
    const items = Array.from(e.dataTransfer?.items ?? []);
    
    if (shouldAddOrReplaceReceipt && transactionID) {
        const file = files.at(0);
        if (!file) {
            return;
        }
    
        attachmentUploadType.current = 'receipt';
        validateFiles([file], items);
    }
    
    attachmentUploadType.current = 'receipt';
    validateFiles(files, items);

    With:

    const files = getFilesFromClipboardEvent(e);
    const items = Array.from(e.dataTransfer?.items ?? []);
    
    validateReceiptFiles(files, items);

    And update the dependency array (line 192):

    [policy, validateReceiptFiles],

    Step 6: Remove the attachmentUploadType ref (line 70) - it's no longer needed:

    Delete:

    const attachmentUploadType = useRef<'receipt' | 'attachment'>(undefined);

    Step 7: Combine the validation components from both hooks (replace lines 195-200):

    Replace:

    return {
        validateAttachments,
        onReceiptDropped,
        PDFValidationComponent,
        ErrorModal,
    };

    With:

    // Combine PDF validation components from both hooks
    const combinedPDFValidationComponent = useMemo(() => {
        const components = [];
        if (AttachmentPDFValidationComponent) {
            components.push(...(Array.isArray(AttachmentPDFValidationComponent) ? AttachmentPDFValidationComponent : [AttachmentPDFValidationComponent]));
        }
        if (ReceiptPDFValidationComponent) {
            components.push(...(Array.isArray(ReceiptPDFValidationComponent) ? ReceiptPDFValidationComponent : [ReceiptPDFValidationComponent]));
        }
        return components.length > 0 ? components : undefined;
    }, [AttachmentPDFValidationComponent, ReceiptPDFValidationComponent]);
    
    // Use receipt error modal by default, fall back to attachment error modal
    const combinedErrorModal = useMemo(() => ReceiptErrorModal ?? AttachmentErrorModal, [AttachmentErrorModal, ReceiptErrorModal]);
    
    return {
        validateAttachments,
        onReceiptDropped,
        PDFValidationComponent: combinedPDFValidationComponent,
        ErrorModal: combinedErrorModal,
    };
  4. paulnjs commented on Oct 13, 2025

    @paulnjs
    Contributor

    Proposal

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

    • Expense - Scan expense with video as receipt can be created by dropping video to Scan zone

    What is the root cause of that problem?

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

                    validateFiles([file], items, true);  // In other places call validateFiles([file], items, false)
    
    validateAndResizeFiles(files, items ?? [], isValidatingReceipts)
    

    , then pass down to

    Promise.all(files.map((file, index) => isValidFile(file, items.at(index), files.length > 1).then((isValid) => (isValid ? file : null))))

    Promise.all(files.map((file, index) => isValidFile(file, items.at(index), files.length > 1, isValidatingReceipts).then((isValid) => (isValid ? file : null))))
    
    • Finally, that value will be used in
      const error = validateAttachment(normalizedFile, isCheckingMultipleFiles, isValidatingReceipts);
      to do the correct validation process

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

    NA

    What alternative solutions did you explore? (Optional)

  5. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 13, 2025
  6. changed the title [-]Expense - Scan expense with video as receipt can be created by dropping video to Scan zone[/-] [+][$250] Expense - Scan expense with video as receipt can be created by dropping video to Scan zone[/+] on Oct 13, 2025
  7. melvin-bot commented on Oct 13, 2025

    @melvin-bot
  8. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Oct 13, 2025
  9. melvin-bot commented on Oct 13, 2025

    @melvin-bot

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

  10. moved this to Bugs and Follow Up Issues in #expensify-bugson Oct 13, 2025
  11. Burhan-Rashid commented on Oct 14, 2025

    @Burhan-Rashid
    Contributor

    Proposal

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

    Expense - Scan expense with video as receipt can be created by dropping video to Scan zone

    What is the root cause of that problem?

    We have recently updated the default value of the hook to not validate receipts by default here:
    852b073

    const {validateFiles, PDFValidationComponent, ErrorModal} = useFilesValidation(onFilesValidated, false);

    The issue is that we are using the same function validateFiles for validating both receipts and attachments but at the time of hook instialiazation we are passing the value of false to isValidatingReceipts for the hook and after that if we have receipt file as well, it will be passed through the validation logic of attachment and not receipt. And for attachments video files are allowed, so it allows the video files to pass through the validation.

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

    We can add a extra optional parameter isValidatingReceiptFiles to the function validateFiles, so that whenever we need to validate receipts, we can check value of the optional parameter first and also we can have a fallback to the default value of the hook parameter which is used during initialization, if the optional parameter is not passed. We need to pass this optional parameter through the chain of functions to finally reach to validateAttachment function where the logic to validate receipts and attachments is finally handled.
    We need to add the following changes at the following places:

    const validateFiles = (files: FileObject[], items?: DataTransferItem[]) => {

    const validateFiles = (files: FileObject[], items?: DataTransferItem[], isValidatingReceiptFiles?: boolean) => {

    validateAndResizeFiles(files, items ?? []);

    validateAndResizeFiles(files, items ?? [], isValidatingReceiptFiles);

    const validateAndResizeFiles = (files: FileObject[], items: DataTransferItem[]) => {

    const validateAndResizeFiles = (files: FileObject[], items: DataTransferItem[], isValidatingReceiptFiles?: boolean) => {

    Promise.all(files.map((file, index) => isValidFile(file, items.at(index), files.length > 1).then((isValid) => (isValid ? file : null))))

    Promise.all(files.map((file, index) => isValidFile(file, items.at(index), files.length > 1, isValidatingReceiptFiles).then((isValid) => (isValid ? file : null))))

    const isValidFile = (originalFile: FileObject, item: DataTransferItem | undefined, isCheckingMultipleFiles?: boolean) => {

    const isValidFile = (originalFile: FileObject, item: DataTransferItem | undefined, isCheckingMultipleFiles?: boolean, isValidatingReceiptFiles?: boolean) => {

    And here we need to use initial hook value as fallback i.e isValidatingReceipts

    const error = validateAttachment(normalizedFile, isCheckingMultipleFiles, isValidatingReceipts);

    const error = validateAttachment(normalizedFile, isCheckingMultipleFiles, isValidatingReceiptFiles ?? isValidatingReceipts);

    And finally pass it true whereever we are validating receipts files:

    validateFiles([file], items, true);

    validateFiles([file], items, true);

    What alternative solutions did you explore? (Optional)

    We could use two hook initialisations one for validating reciepts and one for attachments but i don't think that will be a good practice.
    Note for C+ @parasharrajat : Since there are lot of small changes, i have already added my changes to my local branch for testing.
    Local branch for testing: https://github.com/Burhan-Rashid/App/tree/fix/72402
    Local PR for viewing the diff properly: https://github.com/Burhan-Rashid/App/pull/1/files
    Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.

  12. 26 remaining items

  13. kadiealexander commented on Oct 30, 2025

    @kadiealexander
    Contributor

    Since this was a regression and both contributor and C+ from the original issue did the fix, no payment is required here. @parasharrajat does that sound right to you?

  14. parasharrajat commented on Oct 30, 2025

    @parasharrajat
    Member

    @kadiealexander, This was handled outside the regression period and exported, so it should be eligible for payment. The PR that caused this is part of a major refactoring, and multiple PRs have been created on that task, so I expect this to be eligible for payment.

  15. kadiealexander commented on Oct 31, 2025

    @kadiealexander
    Contributor

    @parasharrajat sounds good, could you please complete the checklist?

  16. parasharrajat commented on Oct 31, 2025

    @parasharrajat
    Member

    BugZero Checklist:

    • [Contributor] Classify the bug:
    Bug classification

    Source of bug:

    • 1a. Result of the original design (eg. a case wasn't considered)
    • 1b. Mistake during implementation
    • 1c. Backend bug
    • 1z. Other:

    Where bug was reported:

    • 2a. Reported on production (eg. bug slipped through the normal regression and PR testing process on staging)
    • 2b. Reported on staging (eg. found during regression or PR testing)
    • 2d. Reported on a PR
    • 2z. Other:

    Who reported the bug:

    • 3a. Expensify user
    • 3b. Expensify employee
    • 3c. Contributor
    • 3d. QA
    • 3z. Other:
    • [Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to comment: refactor: Migrate "Add Attachment" flows to AttachmentModalScreen #69523

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: Not needed

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    • [BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.

      Link to issue: https://github.com/Expensify/Expensify/issues/566446

    Regression Test Proposal

    Precondition:

    Test:

    1. Go to a report
    2. Drop a video file (e.g. .mp4) in the right side (Scan receipt) of the Drag&Drop zone of the composer
    3. Make sure the video get's rejected and there's an error about unsupported file type shown
    4. Drop the same video into the left side of the Drag&Drop zone
    5. Make sure the attachment modal is shown.

    Do we agree 👍 or 👎

  17. changed the title [-][Due for payment 2025-10-30] [$250] Expense - Scan expense with video as receipt can be created by dropping video to Scan zone[/-] [+][Due for payment 2025-11-07] [Due for payment 2025-10-30] [$250] Expense - Scan expense with video as receipt can be created by dropping video to Scan zone[/+] on Oct 31, 2025
  18. melvin-bot commented on Nov 7, 2025

    @melvin-bot

    Payment Summary

    Upwork Job

    BugZero Checklist (@kadiealexander)

    • (if NewFeature) I have created a PR for any necessary HelpDot updates, or I confirmed no updates are necessary.
    • I have verified the correct assignees and roles are listed above and updated the necessary manual offers
    • I have verified that there are no duplicate or incorrect contracts on Upwork for this job (https://www.upwork.com/ab/applicants/1977603641386532131/hired)
    • I have verified the PR was not reverted
    • I have applied any discounts due to bugs/regressions introduced by this PR
    • I have paid out the Upwork contracts or cancelled the ones that are incorrect
    • I have verified the payment summary above is correct
  19. moved this from Bugs and Follow Up Issues to Done in #expensify-bugson Nov 10, 2025
  20. deleted a comment from melvin-bot on Nov 10, 2025
  21. parasharrajat commented on Nov 10, 2025

    @parasharrajat
    Member

    Payment requested as per #72402 (comment)

  22. trjExpensify commented on Nov 17, 2025

    @trjExpensify
    Contributor

    approved $250 for @parasharrajat.

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions